讨论

PreviousNext
Loading…

功能特点

  • 用户管理: 存储和管理带有头像和名称的用户数据
  • 讨论线程: 管理带有评论的讨论数据结构
  • 当前用户跟踪: 跟踪当前活跃用户以进行协作
  • 数据存储: 用于存储协作状态的纯 UI 插件
  • 选择器 API: 通过插件选择器轻松访问用户数据

Kit 使用

安装

添加讨论功能最快的方法是使用 DiscussionKit,它包含预配置的 discussionPlugin 及其 Plate UI 组件。

'use client';
 
import type { TComment } from '@/components/ui/comment';
 
import { createPlatePlugin } from 'platejs/react';
 
import { BlockDiscussion } from '@/components/ui/block-discussion';
 
export type TDiscussion = {
  id: string;
  comments: TComment[];
  createdAt: Date;
  isResolved: boolean;
  userId: string;
  documentContent?: string;
};
 
const BLOCK_SUGGESTION_SELECTOR = '[data-block-suggestion="true"]';
 
const getTargetElement = (target: EventTarget | null) => {
  if (target instanceof HTMLElement) return target;
  if (target instanceof Node) return target.parentElement;
 
  return null;
};
 
export const getDiscussionClickTarget = ({
  selector,
  target,
}: {
  selector: string;
  target: EventTarget | null;
}) => {
  const element = getTargetElement(target);
 
  if (!element) return null;
 
  return element.closest(selector) as HTMLElement | null;
};
 
export const getDiscussionBlockClickTarget = ({
  selector = BLOCK_SUGGESTION_SELECTOR,
  target,
}: {
  selector?: string;
  target: EventTarget | null;
}) =>
  getDiscussionClickTarget({
    selector,
    target,
  });
 
const discussionsData: TDiscussion[] = [
  {
    id: 'discussion1',
    comments: [
      {
        id: 'comment1',
        contentRich: [
          {
            children: [
              {
                text: 'Comments are a great way to provide feedback and discuss changes.',
              },
            ],
            type: 'p',
          },
        ],
        createdAt: new Date(Date.now() - 600_000),
        discussionId: 'discussion1',
        isEdited: false,
        userId: 'charlie',
      },
      {
        id: 'comment2',
        contentRich: [
          {
            children: [
              {
                text: 'Agreed! The link to the docs makes it easy to learn more.',
              },
            ],
            type: 'p',
          },
        ],
        createdAt: new Date(Date.now() - 500_000),
        discussionId: 'discussion1',
        isEdited: false,
        userId: 'bob',
      },
    ],
    createdAt: new Date(),
    documentContent: 'comments',
    isResolved: false,
    userId: 'charlie',
  },
  {
    id: 'discussion2',
    comments: [
      {
        id: 'comment1',
        contentRich: [
          {
            children: [
              {
                text: 'Nice demonstration of overlapping annotations with both comments and suggestions!',
              },
            ],
            type: 'p',
          },
        ],
        createdAt: new Date(Date.now() - 300_000),
        discussionId: 'discussion2',
        isEdited: false,
        userId: 'bob',
      },
      {
        id: 'comment2',
        contentRich: [
          {
            children: [
              {
                text: 'This helps users understand how powerful the editor can be.',
              },
            ],
            type: 'p',
          },
        ],
        createdAt: new Date(Date.now() - 200_000),
        discussionId: 'discussion2',
        isEdited: false,
        userId: 'charlie',
      },
    ],
    createdAt: new Date(),
    documentContent: 'overlapping',
    isResolved: false,
    userId: 'bob',
  },
];
 
const avatarUrl = (seed: string) =>
  `https://api.dicebear.com/9.x/glass/svg?seed=${seed}`;
 
const usersData: Record<
  string,
  { id: string; avatarUrl: string; name: string; hue?: number }
> = {
  alice: {
    id: 'alice',
    avatarUrl: avatarUrl('alice6'),
    name: 'Alice',
  },
  bob: {
    id: 'bob',
    avatarUrl: avatarUrl('bob4'),
    name: 'Bob',
  },
  charlie: {
    id: 'charlie',
    avatarUrl: avatarUrl('charlie2'),
    name: 'Charlie',
  },
};
 
// This plugin is purely UI. It's only used to store the discussions and users data
export const discussionPlugin = createPlatePlugin({
  key: 'discussion',
  options: {
    currentUserId: 'alice',
    discussions: discussionsData,
    users: usersData,
  },
})
  .configure({
    render: { aboveNodes: BlockDiscussion },
  })
  .extendSelectors(({ getOption }) => ({
    currentUser: () => getOption('users')[getOption('currentUserId')],
    user: (id: string) => getOption('users')[id],
  }));
 
export const DiscussionKit = [discussionPlugin];
'use client';
 
import type { TComment } from '@/components/ui/comment';
 
import { createPlatePlugin } from 'platejs/react';
 
import { BlockDiscussion } from '@/components/ui/block-discussion';
 
export type TDiscussion = {
  id: string;
  comments: TComment[];
  createdAt: Date;
  isResolved: boolean;
  userId: string;
  documentContent?: string;
};
 
const BLOCK_SUGGESTION_SELECTOR = '[data-block-suggestion="true"]';
 
const getTargetElement = (target: EventTarget | null) => {
  if (target instanceof HTMLElement) return target;
  if (target instanceof Node) return target.parentElement;
 
  return null;
};
 
export const getDiscussionClickTarget = ({
  selector,
  target,
}: {
  selector: string;
  target: EventTarget | null;
}) => {
  const element = getTargetElement(target);
 
  if (!element) return null;
 
  return element.closest(selector) as HTMLElement | null;
};
 
export const getDiscussionBlockClickTarget = ({
  selector = BLOCK_SUGGESTION_SELECTOR,
  target,
}: {
  selector?: string;
  target: EventTarget | null;
}) =>
  getDiscussionClickTarget({
    selector,
    target,
  });
 
const discussionsData: TDiscussion[] = [
  {
    id: 'discussion1',
    comments: [
      {
        id: 'comment1',
        contentRich: [
          {
            children: [
              {
                text: 'Comments are a great way to provide feedback and discuss changes.',
              },
            ],
            type: 'p',
          },
        ],
        createdAt: new Date(Date.now() - 600_000),
        discussionId: 'discussion1',
        isEdited: false,
        userId: 'charlie',
      },
      {
        id: 'comment2',
        contentRich: [
          {
            children: [
              {
                text: 'Agreed! The link to the docs makes it easy to learn more.',
              },
            ],
            type: 'p',
          },
        ],
        createdAt: new Date(Date.now() - 500_000),
        discussionId: 'discussion1',
        isEdited: false,
        userId: 'bob',
      },
    ],
    createdAt: new Date(),
    documentContent: 'comments',
    isResolved: false,
    userId: 'charlie',
  },
  {
    id: 'discussion2',
    comments: [
      {
        id: 'comment1',
        contentRich: [
          {
            children: [
              {
                text: 'Nice demonstration of overlapping annotations with both comments and suggestions!',
              },
            ],
            type: 'p',
          },
        ],
        createdAt: new Date(Date.now() - 300_000),
        discussionId: 'discussion2',
        isEdited: false,
        userId: 'bob',
      },
      {
        id: 'comment2',
        contentRich: [
          {
            children: [
              {
                text: 'This helps users understand how powerful the editor can be.',
              },
            ],
            type: 'p',
          },
        ],
        createdAt: new Date(Date.now() - 200_000),
        discussionId: 'discussion2',
        isEdited: false,
        userId: 'charlie',
      },
    ],
    createdAt: new Date(),
    documentContent: 'overlapping',
    isResolved: false,
    userId: 'bob',
  },
];
 
const avatarUrl = (seed: string) =>
  `https://api.dicebear.com/9.x/glass/svg?seed=${seed}`;
 
const usersData: Record<
  string,
  { id: string; avatarUrl: string; name: string; hue?: number }
> = {
  alice: {
    id: 'alice',
    avatarUrl: avatarUrl('alice6'),
    name: 'Alice',
  },
  bob: {
    id: 'bob',
    avatarUrl: avatarUrl('bob4'),
    name: 'Bob',
  },
  charlie: {
    id: 'charlie',
    avatarUrl: avatarUrl('charlie2'),
    name: 'Charlie',
  },
};
 
// This plugin is purely UI. It's only used to store the discussions and users data
export const discussionPlugin = createPlatePlugin({
  key: 'discussion',
  options: {
    currentUserId: 'alice',
    discussions: discussionsData,
    users: usersData,
  },
})
  .configure({
    render: { aboveNodes: BlockDiscussion },
  })
  .extendSelectors(({ getOption }) => ({
    currentUser: () => getOption('users')[getOption('currentUserId')],
    user: (id: string) => getOption('users')[id],
  }));
 
export const DiscussionKit = [discussionPlugin];

添加 Kit

import { createPlateEditor } from 'platejs/react';
import { DiscussionKit } from '@/components/editor/plugins/discussion-kit';
 
const editor = createPlateEditor({
  plugins: [
    // ...其他插件,
    ...DiscussionKit,
  ],
});
import { createPlateEditor } from 'platejs/react';
import { DiscussionKit } from '@/components/editor/plugins/discussion-kit';
 
const editor = createPlateEditor({
  plugins: [
    // ...其他插件,
    ...DiscussionKit,
  ],
});

手动使用

安装

pnpm add @platejs/comment @platejs/suggestion
pnpm add @platejs/comment @platejs/suggestion

创建插件

import { createPlatePlugin } from 'platejs/react';
import { BlockDiscussion } from '@/components/ui/block-discussion';
 
export interface TDiscussion {
  id: string;
  comments: TComment[];
  createdAt: Date;
  isResolved: boolean;
  userId: string;
  documentContent?: string;
}
 
const usersData = {
  alice: {
    id: 'alice',
    avatarUrl: 'https://api.dicebear.com/9.x/glass/svg?seed=alice6',
    name: 'Alice',
  },
  bob: {
    id: 'bob', 
    avatarUrl: 'https://api.dicebear.com/9.x/glass/svg?seed=bob4',
    name: 'Bob',
  },
};
 
export const discussionPlugin = createPlatePlugin({
  key: 'discussion',
  options: {
    currentUserId: 'alice',
    discussions: [],
    users: usersData,
  },
})
  .configure({
    render: { aboveNodes: BlockDiscussion },
  })
  .extendSelectors(({ getOption }) => ({
    currentUser: () => getOption('users')[getOption('currentUserId')],
    user: (id: string) => getOption('users')[id],
  }));
import { createPlatePlugin } from 'platejs/react';
import { BlockDiscussion } from '@/components/ui/block-discussion';
 
export interface TDiscussion {
  id: string;
  comments: TComment[];
  createdAt: Date;
  isResolved: boolean;
  userId: string;
  documentContent?: string;
}
 
const usersData = {
  alice: {
    id: 'alice',
    avatarUrl: 'https://api.dicebear.com/9.x/glass/svg?seed=alice6',
    name: 'Alice',
  },
  bob: {
    id: 'bob', 
    avatarUrl: 'https://api.dicebear.com/9.x/glass/svg?seed=bob4',
    name: 'Bob',
  },
};
 
export const discussionPlugin = createPlatePlugin({
  key: 'discussion',
  options: {
    currentUserId: 'alice',
    discussions: [],
    users: usersData,
  },
})
  .configure({
    render: { aboveNodes: BlockDiscussion },
  })
  .extendSelectors(({ getOption }) => ({
    currentUser: () => getOption('users')[getOption('currentUserId')],
    user: (id: string) => getOption('users')[id],
  }));
  • options.currentUserId: 当前活跃用户的 ID
  • options.discussions: 讨论数据结构数组
  • options.users: 将用户 ID 映射到用户数据的对象
  • render.aboveNodes: 在节点上方渲染 BlockDiscussion 用于讨论 UI
  • selectors.currentUser: 获取当前用户数据
  • selectors.user: 通过 ID 获取用户数据

添加插件

import { createPlateEditor } from 'platejs/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...其他插件,
    discussionPlugin,
  ],
});
import { createPlateEditor } from 'platejs/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...其他插件,
    discussionPlugin,
  ],
});

插件

discussionPlugin

用于管理包括用户和讨论数据在内的协作状态的纯 UI 插件。

Options

    协作会话中当前活跃用户的 ID。

    包含评论和元数据的讨论对象数组。

    将用户 ID 映射到包括名称和头像在内的用户信息的对象。

选择器

currentUser

获取当前用户数据。

ReturnsUserData

    当前用户的数据,包括 id、name 和 avatarUrl。

user

通过 ID 获取用户数据。

Parameters

    要查找的用户 ID。

ReturnsUserData | undefined

    如果找到则返回用户数据,否则返回 undefined。

类型

TDiscussion

包含评论和元数据的讨论数据结构。

Attributes

    讨论的唯一标识符。

    讨论线程中的评论数组。

    讨论创建的时间。

    讨论是否已解决。

    创建讨论的用户 ID。

    与此讨论相关的文档内容。

UserData

用于协作的用户信息结构。

Attributes

    用户的唯一标识符。

    用户的显示名称。

    用户头像图片的 URL。

    用于用户识别的可选颜色色调。