评论与业务数据
评论与业务数据的接口、使用示例与行为说明。
本页内容
本页按主题整理项目公开说明,供完成快速接入后查阅。代码片段中的业务回调、服务地址和存储实现需要由宿主提供。
评论功能对接
评论功能由组件负责右侧栏 UI、正文锚点标注、回复交互、图片预览和评论输入框里的 @ 弹层;评论数据本身由业务系统保存。对接时可以把它理解成:组件抛事件,业务保存数据,然后把新的 comments 数组再传回组件。
下面是一个完整的本地演示,包含:
- 当前评论用户假数据
- 已有评论线程假数据
- 评论图片上传 mock
- 评论里的
@用户/@文档查询假数据 - 新建、回复、编辑、删除、完成评论的本地更新逻辑
comment-submit统一通知事件,用来拿到“评论了谁、@了谁、评论内容”等信息
<script setup lang="ts">
import { onBeforeUnmount, ref } from 'vue'
import { RichTextEditor } from '@norio-office/rich-text'
import type {
RichTextEditorCommentCreatePayload,
RichTextEditorCommentDeletePayload,
RichTextEditorCommentMentionProviderPayload,
RichTextEditorCommentReplyPayload,
RichTextEditorCommentResolvePayload,
RichTextEditorCommentSubmitPayload,
RichTextEditorCommentThread,
RichTextEditorCommentUpdatePayload,
RichTextEditorCommentUser,
RichTextEditorMentionItem,
RichTextEditorUploadInput,
RichTextEditorUploadResult,
} from '@norio-office/rich-text'
import '@norio-office/rich-text/style.css'
const content = ref({
type: 'doc',
content: [
{
type: 'heading',
attrs: { level: 1 },
content: [{ type: 'text', text: '评论对接示例文档' }],
},
{
type: 'paragraph',
content: [{ type: 'text', text: '选中这段文字后点击工具栏评论按钮,就可以新建评论。' }],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: '评论输入框里输入 @',
marks: [{ type: 'commentMark', attrs: { threadId: 'thread-demo-1' } }],
},
{ type: 'text', text: ' 可以选择用户或文档。' },
],
},
],
})
const commentUser: RichTextEditorCommentUser = {
id: 'user-current',
name: '当前用户',
color: '#2563eb',
}
const mentionOptions: RichTextEditorMentionItem[] = [
{ id: 'user-001', name: '崔国强', type: 1 },
{ id: 'user-002', name: '张晓明', type: 1 },
{ id: 'user-003', name: '李思雨', type: 1 },
{ id: 'doc-001', name: '产品需求文档', type: 2, tag: '文档', updatedAt: '今天 10:30' },
{ id: 'doc-002', name: '协同功能设计稿', type: 2, tag: '外部', updatedAt: '昨天 19:17' },
{ id: 'doc-003', name: '上线检查清单', type: 2, tag: '模板', updatedAt: '2026-05-17' },
]
const comments = ref<RichTextEditorCommentThread[]>([
{
id: 'thread-demo-1',
anchorText: '评论输入框里输入 @',
status: 'open',
comments: [
{
id: 'comment-demo-1',
content: '这里可以 @张晓明 看一下评论提及。',
mentions: [{ id: 'user-002', name: '张晓明', type: 1 }],
author: { id: 'user-001', name: '崔国强', color: '#4c7dff' },
createdAt: new Date(Date.now() - 10 * 60 * 1000).toISOString(),
},
{
id: 'comment-demo-2',
parentId: 'comment-demo-1',
content: '收到,也可以关联 @产品需求文档。',
mentions: [{ id: 'doc-001', name: '产品需求文档', type: 2, tag: '文档' }],
author: { id: 'user-002', name: '张晓明', color: '#16a34a' },
createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
},
],
},
])
const demoObjectUrls: string[] = []
onBeforeUnmount(() => demoObjectUrls.forEach(url => URL.revokeObjectURL(url)))
async function uploadCommentImage(payload: RichTextEditorUploadInput): Promise<RichTextEditorUploadResult> {
// 真实项目里改成上传到文件服务,然后返回 url/name/size/mimeType。
const url = URL.createObjectURL(payload.file)
demoObjectUrls.push(url)
return {
url,
name: payload.file.name,
size: payload.file.size,
mimeType: payload.file.type,
}
}
async function handleCommentMentionSearch(payload: RichTextEditorCommentMentionProviderPayload): Promise<RichTextEditorMentionItem[]> {
// 真实项目里按 payload.query、payload.type、payload.threadId 等调用接口。
const keyword = payload.query.trim().toLowerCase()
return mentionOptions.filter((item) => {
const typeMatched = payload.type === 'all' || !payload.type || item.type === payload.type
const keywordMatched = !keyword || item.name.toLowerCase().includes(keyword) || item.id.toLowerCase().includes(keyword)
return typeMatched && keywordMatched
})
}
function handleCommentCreate(payload: RichTextEditorCommentCreatePayload) {
comments.value = [
...comments.value,
{
id: payload.threadId,
anchorText: payload.anchorText,
status: 'open',
comments: [
{
id: payload.commentId,
content: payload.content,
images: payload.images,
mentions: payload.mentions,
author: payload.author,
createdAt: payload.createdAt,
},
],
},
]
}
function handleCommentReply(payload: RichTextEditorCommentReplyPayload) {
comments.value = comments.value.map((thread) =>
thread.id === payload.threadId
? {
...thread,
comments: [
...thread.comments,
{
id: payload.commentId,
parentId: payload.parentId,
content: payload.content,
images: payload.images,
mentions: payload.mentions,
author: payload.author,
createdAt: payload.createdAt,
},
],
}
: thread,
)
}
function handleCommentUpdate(payload: RichTextEditorCommentUpdatePayload) {
comments.value = comments.value.map((thread) =>
thread.id === payload.threadId
? {
...thread,
comments: thread.comments.map((item) =>
item.id === payload.commentId
? {
...item,
content: payload.content,
images: payload.images ?? item.images,
mentions: payload.mentions,
updatedAt: payload.updatedAt,
}
: item,
),
}
: thread,
)
}
function handleCommentDelete(payload: RichTextEditorCommentDeletePayload) {
if (!payload.commentId) {
comments.value = comments.value.filter((thread) => thread.id !== payload.threadId)
return
}
comments.value = comments.value
.map((thread) =>
thread.id === payload.threadId
? {
...thread,
comments: thread.comments.filter((item) => item.id !== payload.commentId && item.parentId !== payload.commentId),
}
: thread,
)
.filter((thread) => thread.comments.length > 0)
}
function handleCommentResolve(payload: RichTextEditorCommentResolvePayload) {
comments.value = comments.value.map((thread) =>
thread.id === payload.threadId
? { ...thread, status: payload.resolved ? 'resolved' : 'open' }
: thread,
)
}
function handleCommentSubmit(payload: RichTextEditorCommentSubmitPayload) {
// 这个事件适合做通知、消息流、审计日志。
// payload.content:评论内容
// payload.mentions:本次评论 @ 的用户/文档
// payload.targetUserId / targetCommentId:回复别人时,被回复用户和评论 ID
console.log('comment submit:', payload)
}
</script>
<template>
<RichTextEditor
v-model="content"
:comments="comments"
:comment-user="commentUser"
:show-comments="true"
:upload-comment-image="uploadCommentImage"
:comment-mention-provider="handleCommentMentionSearch"
@comment-create="handleCommentCreate"
@comment-reply="handleCommentReply"
@comment-update="handleCommentUpdate"
@comment-delete="handleCommentDelete"
@comment-resolve="handleCommentResolve"
@comment-submit="handleCommentSubmit"
/>
</template>
评论 Props
上例数据只在内存中更新,刷新页面会丢失;mock 图片地址只在本次页面生命周期有效。生产中必须换成持久化接口和长期可访问的图片 URL。comment-submit 是同一次操作的汇总通知,不要与 comment-create/reply/update 再重复写入同一条评论。用户身份和编辑/删除权限仍应由后端校验。
| Prop | 类型 | 说明 |
|---|---|---|
comments |
RichTextEditorCommentThread[] | null |
评论线程列表,业务方持久化并回传。 |
commentUser |
RichTextEditorCommentUser | null |
当前评论用户;未传时尝试使用协同用户,两者都不可用时不能新建或回复。 |
showComments |
boolean |
是否显示评论功能;为 true 时显示工具栏评论按钮和右侧评论栏,为 false 或不传时隐藏评论入口和评论栏。 |
uploadCommentImage |
RichTextEditorUploadHook | null |
评论图片上传 hook,返回结构同 uploadImage;未传时会回退使用 uploadImage。 |
commentMention |
boolean |
是否启用评论输入框里的 @ 提及,默认启用。 |
commentMentionProvider |
RichTextEditorCommentMentionProvider | null |
评论 @ 查询数据源,独立于正文 mentionProvider。 |
onCommentMentionSearch |
RichTextEditorCommentMentionProvider | null |
评论 @ 查询函数的另一种传入方式,独立于正文 onMentionSearch。 |
评论事件
| 事件 | Payload | 说明 |
|---|---|---|
comment-create |
RichTextEditorCommentCreatePayload |
用户选中文字并发布第一条评论时触发。 |
comment-reply |
RichTextEditorCommentReplyPayload |
用户在已有评论线程下回复时触发;回复某人时会带 parentId、targetUserId、targetCommentId。 |
comment-update |
RichTextEditorCommentUpdatePayload |
用户编辑评论内容时触发。 |
comment-delete |
RichTextEditorCommentDeletePayload |
删除单条评论或整个评论线程时触发。 |
comment-resolve |
RichTextEditorCommentResolvePayload |
用户点击完成评论线程时触发。 |
comment-select |
RichTextEditorCommentThread |
用户点击正文标注或侧边栏评论卡片时触发。 |
comment-submit |
RichTextEditorCommentSubmitPayload |
新建、回复、编辑评论后都会触发;会带 content、mentions、targetUserId、targetCommentId 等通知所需信息。 |
comment-mention-search |
RichTextEditorCommentMentionProviderPayload |
评论输入框触发 @ 查询时通知外部;实际返回数据请使用 commentMentionProvider 或 onCommentMentionSearch。 |
comment-mention-item-click |
RichTextEditorCommentMentionItem |
用户点击评论 @ 弹层里的某个用户/文档时触发。 |
comment-mention-submit |
RichTextEditorCommentMentionItem |
用户确认选择评论 @ 用户/文档时触发。 |
数据保存建议
- 评论锚点是正文内容的一部分,会随
v-modelJSON 或 Yjs 协同内容保存。 - 评论列表不写入正文 JSON,建议后端按
documentId + threadId独立保存。 - 新建评论时,组件会先给选区写入
threadId标注;业务方收到comment-create后应保存同一个threadId并回传到comments。 - 回复某一条评论时,业务方保存回复项时保留
parentId,组件会把它展示在被回复评论下面。 - 评论内容里的
@用户/@文档以普通文本插入到content,同时会通过mentions数组把{ id, name, type }等结构化信息抛出,业务方建议一起保存。 - 评论
@的查询通道与正文提及通道完全独立,接入评论提及时不要复用mention-search事件。 - 多人协同时,正文锚点跟随 Yjs 同步;评论数据需要业务后端自行广播或刷新。
commentUser可以不传;组件会优先使用commentUser,没有时回退使用collaboration.user。如果两者都没有,新建评论和回复按钮会禁用。- 实例方法
focusCommentThread(threadId): boolean可从外部定位到指定评论锚点。
内容依据组件公开文档 · 2026-10-25常见问题