From 4b151c3d8b1c974e07039f7a0dc051252be173c5 Mon Sep 17 00:00:00 2001 From: fantasticit Date: Tue, 5 Apr 2022 13:33:45 +0800 Subject: [PATCH] feat: support mention --- packages/client/package.json | 1 + .../src/components/document/editor/editor.tsx | 81 ++++++++++- .../src/components/document/editor/users.tsx | 93 ++++++++++++ packages/client/src/data/document.ts | 21 ++- packages/client/src/data/user.tsx | 4 +- packages/client/src/services/user.ts | 6 +- .../client/src/tiptap/extensions/mention.ts | 95 ++++++++++++ packages/client/src/tiptap/start-kit.tsx | 3 + packages/client/src/tiptap/styles/index.scss | 1 + .../client/src/tiptap/styles/mention.scss | 7 + .../client/src/tiptap/utils/find-mention.ts | 22 +++ .../wrappers/mention-list/index.module.scss | 42 ++++++ .../tiptap/wrappers/mention-list/index.tsx | 137 ++++++++++++++++++ .../src/tiptap/wrappers/menu-list/index.tsx | 5 - packages/domains/lib/models/document.d.ts | 1 + packages/domains/src/models/document.ts | 1 + .../server/src/controllers/user.controller.ts | 9 ++ packages/server/src/services/user.service.ts | 11 ++ pnpm-lock.yaml | 13 ++ 19 files changed, 525 insertions(+), 28 deletions(-) create mode 100644 packages/client/src/components/document/editor/users.tsx create mode 100644 packages/client/src/tiptap/extensions/mention.ts create mode 100644 packages/client/src/tiptap/styles/mention.scss create mode 100644 packages/client/src/tiptap/utils/find-mention.ts create mode 100644 packages/client/src/tiptap/wrappers/mention-list/index.module.scss create mode 100644 packages/client/src/tiptap/wrappers/mention-list/index.tsx diff --git a/packages/client/package.json b/packages/client/package.json index 3f12c981..822b3e1a 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -35,6 +35,7 @@ "@tiptap/extension-italic": "^2.0.0-beta.25", "@tiptap/extension-link": "^2.0.0-beta.36", "@tiptap/extension-list-item": "^2.0.0-beta.20", + "@tiptap/extension-mention": "^2.0.0-beta.95", "@tiptap/extension-ordered-list": "^2.0.0-beta.27", "@tiptap/extension-paragraph": "^2.0.0-beta.23", "@tiptap/extension-placeholder": "^2.0.0-beta.47", diff --git a/packages/client/src/components/document/editor/editor.tsx b/packages/client/src/components/document/editor/editor.tsx index 1b4d8b7d..074adb5f 100644 --- a/packages/client/src/components/document/editor/editor.tsx +++ b/packages/client/src/components/document/editor/editor.tsx @@ -1,4 +1,5 @@ -import React, { useMemo, useEffect, useState } from 'react'; +import Router from 'next/router'; +import React, { useMemo, useEffect, useState, useRef } from 'react'; import cls from 'classnames'; import { useEditor, EditorContent } from '@tiptap/react'; import { BackTop } from '@douyinfe/semi-ui'; @@ -17,10 +18,13 @@ import { getIndexdbProvider, destoryIndexdbProvider, } from 'tiptap'; +import { findMentions } from 'tiptap/utils/find-mention'; +import { useCollaborationDocument } from 'data/document'; import { DataRender } from 'components/data-render'; import { Banner } from 'components/banner'; import { debounce } from 'helpers/debounce'; import { event, triggerChangeDocumentTitle, triggerJoinUser, USE_DOCUMENT_VERSION } from 'event'; +import { DocumentUserSetting } from './users'; import styles from './index.module.scss'; interface IProps { @@ -31,8 +35,10 @@ interface IProps { style: React.CSSProperties; } -export const Editor: React.FC = ({ user, documentId, authority, className, style }) => { - if (!user) return null; +export const Editor: React.FC = ({ user: currentUser, documentId, authority, className, style }) => { + if (!currentUser) return null; + const $hasShowUserSettingModal = useRef(false); + const { users, addUser, updateUser } = useCollaborationDocument(documentId); const [status, setStatus] = useState('connecting'); const { online } = useNetwork(); const [loading, toggleLoading] = useToggle(true); @@ -40,9 +46,9 @@ export const Editor: React.FC = ({ user, documentId, authority, classNam const provider = useMemo(() => { return getProvider({ targetId: documentId, - token: user.token, + token: currentUser.token, cacheType: 'EDITOR', - user, + user: currentUser, docType: 'document', events: { onAwarenessUpdate({ states }) { @@ -57,15 +63,14 @@ export const Editor: React.FC = ({ user, documentId, authority, classNam }, }, }); - }, [documentId, user.token]); - + }, [documentId, currentUser.token]); const editor = useEditor({ editable: authority && authority.editable, extensions: [ ...BaseKit, DocumentWithTitle, getCollaborationExtension(provider), - getCollaborationCursorExtension(provider, user), + getCollaborationCursorExtension(provider, currentUser), ], onTransaction: debounce(({ transaction }) => { try { @@ -74,6 +79,8 @@ export const Editor: React.FC = ({ user, documentId, authority, classNam } catch (e) {} }, 50), }); + const [mentionUsersSettingVisible, toggleMentionUsersSettingVisible] = useToggle(false); + const [mentionUsers, setMentionUsers] = useState([]); useEffect(() => { const indexdbProvider = getIndexdbProvider(documentId, provider.document); @@ -96,11 +103,61 @@ export const Editor: React.FC = ({ user, documentId, authority, classNam if (!editor) return; const handler = (data) => editor.commands.setContent(data); event.on(USE_DOCUMENT_VERSION, handler); + return () => { event.off(USE_DOCUMENT_VERSION, handler); }; }, [editor]); + useEffect(() => { + if (!editor) return; + + const handler = () => { + // 已经拦截过一次,不再拦截 + if ($hasShowUserSettingModal.current) return; + + const mentionUsers = findMentions(editor); + if (!mentionUsers || !mentionUsers.length) return; + + const currentUserAuth = users.find((user) => { + return user.user.name === currentUser.name; + }); + const isCurrentUserCreateUser = currentUserAuth.auth.createUserId === currentUser.id; + + if (!isCurrentUserCreateUser) { + return; + } + + const data = Array.from(new Set(mentionUsers)) + .filter((userName) => { + const exist = users.find((user) => { + return user.user.name === userName; + }); + if (!exist || !exist.auth.readable) return true; + return false; + }) + .filter(Boolean); + + if (!data.length) return; + + setMentionUsers(data); + toggleMentionUsersSettingVisible(true); + $hasShowUserSettingModal.current = true; + // ignore-me + const newErr = new Error('请完成权限操作后关闭页面'); + throw newErr; + }; + + Router.events.on('routeChangeStart', handler); + window.addEventListener('unload', handler); + + return () => { + $hasShowUserSettingModal.current = false; + Router.events.off('routeChangeStart', handler); + window.removeEventListener('unload', handler); + }; + }, [editor, users, currentUser]); + return ( = ({ user, documentId, authority, classNam document.querySelector('#js-template-editor-container')} /> + ); }} diff --git a/packages/client/src/components/document/editor/users.tsx b/packages/client/src/components/document/editor/users.tsx new file mode 100644 index 00000000..fca51f0b --- /dev/null +++ b/packages/client/src/components/document/editor/users.tsx @@ -0,0 +1,93 @@ +import React, { useMemo } from 'react'; +import { Modal, Typography, Table, Checkbox } from '@douyinfe/semi-ui'; +import { IAuthority, IUser } from '@think/domains'; +import { DocAuth } from 'data/document'; + +interface IProps { + visible: boolean; + toggleVisible: (arg: boolean) => void; + mentionUsers: string[]; + users: Array<{ user: IUser; auth: IAuthority }>; + addUser: (auth: DocAuth) => Promise; + updateUser: (auth: DocAuth) => Promise; +} + +const { Text } = Typography; +const { Column } = Table; + +const renderChecked = (onChange, authKey: 'readable' | 'editable') => (checked, data) => { + const handle = (evt) => { + const ret = { + ...data, + }; + ret[authKey] = evt.target.checked; + onChange(ret); + }; + return ; +}; + +export const DocumentUserSetting: React.FC = ({ + visible, + toggleVisible, + mentionUsers, + users, + addUser, + updateUser, +}) => { + const renderUsers = useMemo(() => { + return mentionUsers + .map((mentionUser) => { + const exist = users.find((user) => { + return user.user.name === mentionUser; + }); + + if (!exist) return { userName: mentionUser, readable: false, editable: false, shouldAddToDocument: true }; + + return { + userName: mentionUser, + readable: exist.auth.readable, + editable: exist.auth.editable, + shouldAddToDocument: false, + }; + }) + .filter(Boolean); + }, [users, mentionUsers]); + + const handler = async (data) => { + if (data.shouldAddToDocument) { + await addUser(data.userName); + } + + await updateUser(data); + }; + + return ( + toggleVisible(false)} + maskClosable={false} + style={{ maxWidth: '96vw' }} + footer={null} + > + 您在该文档中 @ 了以下用户,请为他们操作权限,否则他们无法阅读该文档。 + + + + +
+
+ ); +}; diff --git a/packages/client/src/data/document.ts b/packages/client/src/data/document.ts index 6fb713a6..9442d064 100644 --- a/packages/client/src/data/document.ts +++ b/packages/client/src/data/document.ts @@ -1,18 +1,10 @@ -import type { IUser, IDocument, IWiki } from '@think/domains'; +import type { IUser, IDocument, IWiki, IAuthority } from '@think/domains'; import useSWR from 'swr'; import { useState, useCallback, useEffect } from 'react'; import { useAsyncLoading } from 'hooks/use-async-loading'; import { HttpClient } from 'services/http-client'; import { getPublicDocumentDetail } from 'services/document'; -interface IAuthority { - id: string; - documentId: string; - userId: string; - readable: boolean; - editable: boolean; -} - type ICreateDocument = Partial>; type IDocumentWithAuth = { document: IDocument; authority: IAuthority }; type IUpdateDocument = Partial>; @@ -171,13 +163,18 @@ export const usePublicDocument = (documentId: string) => { }; }; +export type DocAuth = { + userName: string; + readable?: boolean; + editable?: boolean; +}; /** * 协作文档 * @param documentId * @returns */ export const useCollaborationDocument = (documentId) => { - const { data, error, mutate } = useSWR>( + const { data, error, mutate } = useSWR>( `/document/user/${documentId}`, (url) => HttpClient.get(url), { shouldRetryOnError: false } @@ -195,7 +192,7 @@ export const useCollaborationDocument = (documentId) => { return ret; }; - const updateUser = async (docAuth) => { + const updateUser = async (docAuth: DocAuth) => { const ret = await HttpClient.post(`/document/user/${documentId}/update`, { documentId, ...docAuth, @@ -204,7 +201,7 @@ export const useCollaborationDocument = (documentId) => { return ret; }; - const deleteUser = async (docAuth) => { + const deleteUser = async (docAuth: DocAuth) => { const ret = await HttpClient.post(`/document/user/${documentId}/delete`, { documentId, ...docAuth, diff --git a/packages/client/src/data/user.tsx b/packages/client/src/data/user.tsx index 2cf8099c..cf08304e 100644 --- a/packages/client/src/data/user.tsx +++ b/packages/client/src/data/user.tsx @@ -7,7 +7,7 @@ import { getStorage, setStorage } from 'helpers/storage'; export const useUser = () => { const router = useRouter(); - const { data, error, mutate } = useSWR('user', getStorage); + const { data, error, mutate } = useSWR('user', getStorage); const logout = useCallback(() => { window.localStorage.removeItem('user'); @@ -29,7 +29,7 @@ export const useUser = () => { const updateUser = async (patch: Pick) => { const res = await HttpClient.patch('/user/update', patch); - const ret = { ...data, ...res } as unknown as IUser; + const ret = { ...data, ...res } as unknown as ILoginUser; setStorage('user', JSON.stringify(ret)); mutate(ret); }; diff --git a/packages/client/src/services/user.ts b/packages/client/src/services/user.ts index bd8cb1d5..51278a28 100644 --- a/packages/client/src/services/user.ts +++ b/packages/client/src/services/user.ts @@ -1,6 +1,10 @@ -import type { IUser } from '@think/domains'; +import type { IPagination, IUser } from '@think/domains'; import { HttpClient } from './http-client'; export const register = (data: Partial): Promise => { return HttpClient.post('/user/register', data); }; + +export const getUsers = (): Promise => { + return HttpClient.get('/user'); +}; diff --git a/packages/client/src/tiptap/extensions/mention.ts b/packages/client/src/tiptap/extensions/mention.ts new file mode 100644 index 00000000..51b43f5f --- /dev/null +++ b/packages/client/src/tiptap/extensions/mention.ts @@ -0,0 +1,95 @@ +import BulitInMention from '@tiptap/extension-mention'; +import { ReactRenderer } from '@tiptap/react'; +import tippy from 'tippy.js'; +import { getUsers } from 'services/user'; +import { MentionList } from '../wrappers/mention-list'; + +const suggestion = { + items: async ({ query }) => { + const res = await getUsers(); + const data = res.map((item) => item.name); + return data.filter((item) => item.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5); + }, + + render: () => { + let component; + let popup; + + return { + onStart: (props) => { + component = new ReactRenderer(MentionList, { + props, + editor: props.editor, + }); + + popup = tippy('body', { + getReferenceClientRect: props.clientRect, + appendTo: () => document.body, + content: component.element, + showOnCreate: true, + interactive: true, + trigger: 'manual', + placement: 'bottom-start', + }); + }, + + onUpdate(props) { + component.updateProps(props); + + popup[0].setProps({ + getReferenceClientRect: props.clientRect, + }); + }, + + onKeyDown(props) { + if (props.event.key === 'Escape') { + popup[0].hide(); + + return true; + } + + return component.ref?.onKeyDown(props); + }, + + onExit() { + popup[0].destroy(); + component.destroy(); + }, + }; + }, + + command: ({ editor, range, props }) => { + // increase range.to by one when the next node is of type "text" + // and starts with a space character + const nodeAfter = editor.view.state.selection.$to.nodeAfter; + const overrideSpace = nodeAfter?.text?.startsWith(' '); + + if (overrideSpace) { + range.to += 1; + } + + console.log('mention', props); + + editor + .chain() + .focus() + .insertContentAt(range, [ + { + type: BulitInMention.name, + attrs: props, + }, + { + type: 'text', + text: ' ', + }, + ]) + .run(); + }, +}; + +export const Mention = BulitInMention.configure({ + HTMLAttributes: { + class: 'mention', + }, + suggestion, +}); diff --git a/packages/client/src/tiptap/start-kit.tsx b/packages/client/src/tiptap/start-kit.tsx index 76e75fb2..57141630 100644 --- a/packages/client/src/tiptap/start-kit.tsx +++ b/packages/client/src/tiptap/start-kit.tsx @@ -28,6 +28,7 @@ import { Katex } from './extensions/katex'; import { Link } from './extensions/link'; import { ListItem } from './extensions/listItem'; import { Loading } from './extensions/loading'; +import { Mention } from './extensions/mention'; import { Mind } from './extensions/mind'; import { OrderedList } from './extensions/ordered-list'; import { Paragraph } from './extensions/paragraph'; @@ -84,6 +85,7 @@ export const BaseKit = [ Link, ListItem, Loading, + Mention, Mind, OrderedList, Paragraph, @@ -132,6 +134,7 @@ export const CommentKit = [ Katex, Link, ListItem, + Mention, OrderedList, Paragraph, Placeholder, diff --git a/packages/client/src/tiptap/styles/index.scss b/packages/client/src/tiptap/styles/index.scss index 2944dbfd..ae8767e3 100644 --- a/packages/client/src/tiptap/styles/index.scss +++ b/packages/client/src/tiptap/styles/index.scss @@ -5,6 +5,7 @@ @import './heading.scss'; @import './katex.scss'; @import './list.scss'; +@import './mention.scss'; @import './menu.scss'; @import './mind.scss'; @import './placeholder.scss'; diff --git a/packages/client/src/tiptap/styles/mention.scss b/packages/client/src/tiptap/styles/mention.scss new file mode 100644 index 00000000..9a658af5 --- /dev/null +++ b/packages/client/src/tiptap/styles/mention.scss @@ -0,0 +1,7 @@ +.mention { + color: #fff; + padding: 2px 6px; + background-color: var(--semi-color-primary); + border: 1px solid var(--semi-color-border); + border-radius: 999em; +} diff --git a/packages/client/src/tiptap/utils/find-mention.ts b/packages/client/src/tiptap/utils/find-mention.ts new file mode 100644 index 00000000..36bc3e65 --- /dev/null +++ b/packages/client/src/tiptap/utils/find-mention.ts @@ -0,0 +1,22 @@ +import { Editor } from '@tiptap/core'; +import { Mention } from '../extensions/mention'; + +export const findMentions = (editor: Editor) => { + const content = editor.getJSON(); + const queue = [content]; + const res = []; + + while (queue.length) { + const node = queue.shift(); + + if (node.type === Mention.name) { + res.push(node.attrs.id); + } + + if (node.content && node.content.length) { + queue.push(...node.content); + } + } + + return res; +}; diff --git a/packages/client/src/tiptap/wrappers/mention-list/index.module.scss b/packages/client/src/tiptap/wrappers/mention-list/index.module.scss new file mode 100644 index 00000000..806ea46a --- /dev/null +++ b/packages/client/src/tiptap/wrappers/mention-list/index.module.scss @@ -0,0 +1,42 @@ +.items { + width: 200px; + max-height: 380px; + overflow-x: hidden; + overflow-y: auto; + background-color: var(--semi-color-bg-0); + border-radius: var(--border-radius); + box-shadow: rgb(9 30 66 / 31%) 0 0 1px, rgb(9 30 66 / 25%) 0 4px 8px -2px; +} + +.item { + display: flex; + width: 100%; + padding: 12px 12px 11px; + color: rgb(9 30 66); + text-decoration: none; + cursor: pointer; + background-color: rgb(255 255 255); + border: 0; + border-radius: 0; + outline: 0; + flex: 0 0 auto; + align-items: center; + box-sizing: border-box; + fill: rgb(255 255 255); + + &:hover { + background-color: #f4f5f7; + } + + &.is-selected { + color: rgb(0 82 204); + text-decoration: none; + background-color: rgb(222 235 255); + fill: rgb(222 235 255); + } + + img { + width: 1em; + height: 1em; + } +} diff --git a/packages/client/src/tiptap/wrappers/mention-list/index.tsx b/packages/client/src/tiptap/wrappers/mention-list/index.tsx new file mode 100644 index 00000000..09e5484e --- /dev/null +++ b/packages/client/src/tiptap/wrappers/mention-list/index.tsx @@ -0,0 +1,137 @@ +import React, { useState, useEffect, forwardRef, useImperativeHandle, useRef } from 'react'; +import { Modal, Typography } from '@douyinfe/semi-ui'; +import { useRouter } from 'next/router'; +import { Editor } from '@tiptap/core'; +import cls from 'classnames'; +import scrollIntoView from 'scroll-into-view-if-needed'; +import { useUser } from 'data/user'; +import { useCollaborationDocument } from 'data/document'; +import styles from './index.module.scss'; + +interface IProps { + editor: Editor; + items: Array; + command: any; +} + +const { Title, Text } = Typography; + +export const MentionList: React.FC = forwardRef((props, ref) => { + const $container = useRef(); + const router = useRouter(); + const { user: currentUser } = useUser(); + const { users, addUser, updateUser } = useCollaborationDocument(router.query.documentId); + const [selectedIndex, setSelectedIndex] = useState(0); + + const selectItem = (index) => { + const userName = props.items[index]; + if (!userName) return; + props.command({ id: userName }); + + // const currentUserAuth = users.find((user) => { + // return user.user.name === currentUser.name; + // }); + // const isCurrentUserCreateUser = currentUserAuth.auth.createUserId === currentUser.id; + + // const target = users.find((user) => { + // return user.user.name === userName; + // }); + + // if (isCurrentUserCreateUser) { + // if (!target) { + // Modal.confirm({ + // title: 权限操作, + // content: 当前用户尚未加入该文档,是否添加他?, + // onCancel: () => {}, + // onOk: async () => { + // addUser(userName).then((res) => { + // console.log('用户已经添加', res); + // props.command({ id: userName }); + // }); + // }, + // }); + // } else { + // if (!target.auth.readable) { + // Modal.confirm({ + // title: 权限操作, + // content: 当前用户无法阅读该文档,是否添加阅读权限?, + // onCancel: () => {}, + // onOk: async () => { + // updateUser({ + // userName, + // readable: true, + // editable: target.auth.editable, + // }).then((res) => { + // props.command({ id: userName }); + // }); + // }, + // }); + // } else { + // props.command({ id: userName }); + // } + // } + // } else { + // } + }; + + const upHandler = () => { + setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length); + }; + + const downHandler = () => { + setSelectedIndex((selectedIndex + 1) % props.items.length); + }; + + const enterHandler = () => { + selectItem(selectedIndex); + }; + + useEffect(() => setSelectedIndex(0), [props.items]); + + useEffect(() => { + if (Number.isNaN(selectedIndex + 1)) return; + const el = $container.current.querySelector(`button:nth-of-type(${selectedIndex + 1})`); + el && scrollIntoView(el, { behavior: 'smooth', scrollMode: 'if-needed' }); + }, [selectedIndex]); + + useImperativeHandle(ref, () => ({ + onKeyDown: ({ event }) => { + if (event.key === 'ArrowUp') { + upHandler(); + return true; + } + + if (event.key === 'ArrowDown') { + downHandler(); + return true; + } + + if (event.key === 'Enter') { + enterHandler(); + return true; + } + + return false; + }, + })); + + return ( +
+
+ {props.items.length ? ( + props.items.map((item, index) => ( + + )) + ) : ( +
没有找到结果
+ )} +
+
+ ); +}); diff --git a/packages/client/src/tiptap/wrappers/menu-list/index.tsx b/packages/client/src/tiptap/wrappers/menu-list/index.tsx index b4d4a987..0b17fd14 100644 --- a/packages/client/src/tiptap/wrappers/menu-list/index.tsx +++ b/packages/client/src/tiptap/wrappers/menu-list/index.tsx @@ -12,7 +12,6 @@ interface IProps { export const MenuList: React.FC = forwardRef((props, ref) => { const $container = useRef(); - const $image = useRef(); const [selectedIndex, setSelectedIndex] = useState(0); const selectItem = (index) => { @@ -35,10 +34,6 @@ export const MenuList: React.FC = forwardRef((props, ref) => { selectItem(selectedIndex); }; - const handleSelectImage = function () { - console.log('image', this.files); - }; - useEffect(() => setSelectedIndex(0), [props.items]); useEffect(() => { diff --git a/packages/domains/lib/models/document.d.ts b/packages/domains/lib/models/document.d.ts index 7c94c09b..696e9204 100644 --- a/packages/domains/lib/models/document.d.ts +++ b/packages/domains/lib/models/document.d.ts @@ -27,4 +27,5 @@ export interface IAuthority { userId: IUser['id']; readable: boolean; editable: boolean; + createUserId: IUser['id']; } diff --git a/packages/domains/src/models/document.ts b/packages/domains/src/models/document.ts index cb55e3ea..c5ee906b 100644 --- a/packages/domains/src/models/document.ts +++ b/packages/domains/src/models/document.ts @@ -39,4 +39,5 @@ export interface IAuthority { userId: IUser['id']; readable: boolean; editable: boolean; + createUserId: IUser['id']; } diff --git a/packages/server/src/controllers/user.controller.ts b/packages/server/src/controllers/user.controller.ts index 9a96bc96..c62ebbc2 100644 --- a/packages/server/src/controllers/user.controller.ts +++ b/packages/server/src/controllers/user.controller.ts @@ -7,6 +7,7 @@ import { Request, UseGuards, UseInterceptors, + Get, Patch, ClassSerializerInterceptor, } from '@nestjs/common'; @@ -42,4 +43,12 @@ export class UserController { async updateUser(@Request() req, @Body() dto: UpdateUserDto) { return await this.userService.updateUser(req.user, dto); } + + @UseInterceptors(ClassSerializerInterceptor) + @Get('/') + @HttpCode(HttpStatus.OK) + @UseGuards(JwtGuard) + async getUsers() { + return this.userService.getUsers(); + } } diff --git a/packages/server/src/services/user.service.ts b/packages/server/src/services/user.service.ts index 9c70f055..0eb7fe4a 100644 --- a/packages/server/src/services/user.service.ts +++ b/packages/server/src/services/user.service.ts @@ -149,4 +149,15 @@ export class UserService { const user = this.jwtService.decode(token) as UserEntity; return user; } + + /** + * 获取用户列表 + * @param pagination + * @returns + */ + async getUsers() { + const query = this.userRepo.createQueryBuilder('user'); + const [data] = await query.getManyAndCount(); + return data; + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a16be000..1ffa85eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,6 +61,7 @@ importers: '@tiptap/extension-italic': ^2.0.0-beta.25 '@tiptap/extension-link': ^2.0.0-beta.36 '@tiptap/extension-list-item': ^2.0.0-beta.20 + '@tiptap/extension-mention': ^2.0.0-beta.95 '@tiptap/extension-ordered-list': ^2.0.0-beta.27 '@tiptap/extension-paragraph': ^2.0.0-beta.23 '@tiptap/extension-placeholder': ^2.0.0-beta.47 @@ -143,6 +144,7 @@ importers: '@tiptap/extension-italic': 2.0.0-beta.25_@tiptap+core@2.0.0-beta.171 '@tiptap/extension-link': 2.0.0-beta.36_@tiptap+core@2.0.0-beta.171 '@tiptap/extension-list-item': 2.0.0-beta.20_@tiptap+core@2.0.0-beta.171 + '@tiptap/extension-mention': 2.0.0-beta.95_@tiptap+core@2.0.0-beta.171 '@tiptap/extension-ordered-list': 2.0.0-beta.27_@tiptap+core@2.0.0-beta.171 '@tiptap/extension-paragraph': 2.0.0-beta.23_@tiptap+core@2.0.0-beta.171 '@tiptap/extension-placeholder': 2.0.0-beta.47_@tiptap+core@2.0.0-beta.171 @@ -1740,6 +1742,17 @@ packages: '@tiptap/core': 2.0.0-beta.171 dev: false + /@tiptap/extension-mention/2.0.0-beta.95_@tiptap+core@2.0.0-beta.171: + resolution: {integrity: sha512-AiikYJa33APtMI7c6a4EpPhqAYxHHnub5b9hd62zM1peBp2D2aO1dEIL7cB7O2P8EBZ2pu2QN2mFcCbVlSi0Xw==} + peerDependencies: + '@tiptap/core': ^2.0.0-beta.1 + dependencies: + '@tiptap/core': 2.0.0-beta.171 + '@tiptap/suggestion': 2.0.0-beta.90_@tiptap+core@2.0.0-beta.171 + prosemirror-model: 1.16.1 + prosemirror-state: 1.3.4 + dev: false + /@tiptap/extension-ordered-list/2.0.0-beta.27_@tiptap+core@2.0.0-beta.171: resolution: {integrity: sha512-apFDeignxdZb3cA3p1HJu0zw1JgJdBYUBz1r7f99qdNybYuk3I/1MPUvlOuOgvIrBB/wydoyVDP+v9F7QN3tfQ==} peerDependencies: