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 讓 Enter 送出(Shift / Ctrl / Meta + Enter 換行)。
基礎用法(v-model / v-model:json + Enter 送出)
<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 | Enter 觸發 submit(Shift/Ctrl/Meta+Enter 換行) | |
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 | — | 按 Enter(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。