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 讓 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-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 硬限制,不破壞既有格式)
submitOnEnterbooleanfalseEnter 觸發 submit(Shift/Ctrl/Meta+Enter 換行)
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—按 Enter(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。