SwEditor

SwEditor 是基于 Tiptap(底层 ProseMirror)的富文本编辑器,面向评论 / 发帖 / 聊天等 compose 场景:v-model 双向绑定、@ 提及用户、# 话题、内置 emoji 面板、图 / 视频上传、配菜式工具栏,且SSR 安全——可直接放进 SSR / ISR 页面(编辑器本体只在客户端挂载,不拖累整页渲染、不强迫宿主页 ssr: false)。

视觉走「最小必要基础样式」——编辑框 / 工具栏 / 下拉 / 表情面板默认都肉眼可辨、体面,颜色 / 圆角 / 内距等通过 --sw-editor-* token 或项目 CSS 覆盖。

能力边界:当前实现 compose 形态(文本 + 提及 + 表情 + 上传按钮)。长文 WYSIWYG(标题 / 表格 / 代码块工具栏)属 document 形态,已预留 mode prop、按需再扩展。

基础用法

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-bgvar(--background)编辑区背景
--sw-editor-bordervar(--input)外框边框色(1px 宽度常驻;focus 只变色、零 reflow)
--sw-editor-radiusvar(--radius)外框圆角
--sw-editor-padding0.625rem 0.75rem编辑区内边距
--sw-editor-min-height4rem编辑区最小高度(SSR 占位壳同高,防 hydration 跳动 / CLS)
--sw-editor-mention-colorvar(--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

名称类型必填默认值说明
modelValuestring''v-model,编辑器内容 HTML
json(v-model:json)object | nullnull编辑器内容的 Tiptap JSON
placeholderstring''空状态占位文本
limitnumber—最大字符数(CharacterCount 硬限制,不破坏已有格式)
submitOnEnterbooleanfalse回车触发 submit(Shift/Ctrl/Meta+回车换行)
editablebooleantrue是否可编辑(false 为只读)
autofocusbooleanfalse挂载后自动聚焦
mode'compose' | 'document''compose'形态档位(当前实现 compose;document 预留)
mention{ fetchUsers?, fetchTags?, customTopic? }—提及配置;缺省不启用 @ / #
toolsEditorTool[]—内置工具栏按钮:'mention'|'topic'|'emoji'|'image'|'video'|'submit';缺省不显示工具栏
uploadEditorUploadConfig—image / video 工具的上传函数注入;缺对应配置则不渲染该按钮
stylesRecord<string, string>{}应用到编辑区的内联样式(如 minHeight)
extensionsunknown[]—额外 Tiptap 扩展(薄壳扩展,不改源码)

Events

事件载荷说明
update:modelValuestring内容变化(HTML)
update:jsonobject内容变化(Tiptap JSON)
focus / blur—聚焦 / 失焦
submit—按回车(submitOnEnter 开启时)或点击内置工具栏发送按钮(:tools 含 'submit')时触发
readyEditor编辑器实例就绪
mention-click{ id, type, label }点击已插入的提及(type 为 '@' / '#')
upload{ type, result }图 / 视频上传成功(type 为 'image' / 'video')
upload-errorError上传失败

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。