client: improve perf

This commit is contained in:
fantasticit 2022-09-17 17:17:53 +08:00
parent 64113f48f1
commit a9892a3fa8
25 changed files with 496 additions and 417 deletions

View File

@ -89,7 +89,8 @@ export const ColorPicker: React.FC<{
const [visible, toggleVisible] = useToggle(false); const [visible, toggleVisible] = useToggle(false);
const content = useMemo( const content = useMemo(
() => ( () =>
!visible ? null : (
<div style={{ padding: isMobile ? '24px 0 24px' : '12px 16px', width: isMobile ? 'auto' : 272 }}> <div style={{ padding: isMobile ? '24px 0 24px' : '12px 16px', width: isMobile ? 'auto' : 272 }}>
<div className={styles.emptyWrap} onClick={() => onSetColor(null)}> <div className={styles.emptyWrap} onClick={() => onSetColor(null)}>
<span></span> <span></span>
@ -107,7 +108,7 @@ export const ColorPicker: React.FC<{
</div> </div>
</div> </div>
), ),
[onSetColor, isMobile] [onSetColor, isMobile, visible]
); );
if (disabled) return <span style={{ display: 'inline-block' }}>{children}</span>; if (disabled) return <span style={{ display: 'inline-block' }}>{children}</span>;
@ -132,7 +133,14 @@ export const ColorPicker: React.FC<{
</span> </span>
</> </>
) : ( ) : (
<Dropdown zIndex={10000} trigger="click" position={'bottomLeft'} render={content}> <Dropdown
visible={visible}
onVisibleChange={toggleVisible}
zIndex={10000}
trigger="click"
position={'bottomLeft'}
render={content}
>
<span style={{ display: 'inline-block' }}>{children}</span> <span style={{ display: 'inline-block' }}>{children}</span>
</Dropdown> </Dropdown>
)} )}

View File

@ -4,15 +4,17 @@ import React, { useMemo } from 'react';
const { Text } = Typography; const { Text } = Typography;
export const defaultLoading = <Spin />; export const defaultLoading = (
<div style={{ margin: 'auto' }}>
<Spin />
</div>
);
export const defaultRenderError = (error) => { export const defaultRenderError = (error) => {
return <Text>{(error && error.message) || '未知错误'}</Text>; return <Text>{(error && error.message) || '未知错误'}</Text>;
}; };
export const defaultEmpty = ( const emptyStyle: React.CSSProperties = {
<div
style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@ -21,8 +23,10 @@ export const defaultEmpty = (
top: '50%', top: '50%',
left: '50%', left: '50%',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
}} };
>
export const defaultEmpty = (
<div style={emptyStyle}>
<div> <div>
<Empty /> <Empty />
</div> </div>

View File

@ -1,3 +1,4 @@
import deepEqual from 'deep-equal';
import React from 'react'; import React from 'react';
import { defaultEmpty, defaultLoading, defaultRenderError, Render } from './constant'; import { defaultEmpty, defaultLoading, defaultRenderError, Render } from './constant';
@ -15,7 +16,7 @@ interface IProps {
normalContent: RenderProps; normalContent: RenderProps;
} }
export const DataRender: React.FC<IProps> = ({ export const _DataRender: React.FC<IProps> = ({
loading, loading,
error, error,
empty, empty,
@ -36,3 +37,7 @@ export const DataRender: React.FC<IProps> = ({
<LoadingWrap loading={loading} loadingContent={loadingContent} normalContent={loading ? null : normalContent} /> <LoadingWrap loading={loading} loadingContent={loadingContent} normalContent={loading ? null : normalContent} />
); );
}; };
export const DataRender = React.memo(_DataRender, (prevProps, nextProps) => {
return deepEqual(prevProps, nextProps);
});

View File

@ -16,14 +16,26 @@ interface IProps {
const { Text } = Typography; const { Text } = Typography;
const mobileContainerStyle: React.CSSProperties = { maxWidth: '96vw', maxHeight: '60vh', overflow: 'auto' };
const pcContainerStyle: React.CSSProperties = {
width: 412,
maxWidth: '96vw',
padding: '0 24px',
maxHeight: '60vh',
overflow: 'auto',
};
export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, disabled = false }) => { export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, disabled = false }) => {
const { isMobile } = IsOnMobile.useHook(); const { isMobile } = IsOnMobile.useHook();
const toastedUsersRef = useRef([]); const toastedUsersRef = useRef([]);
const { user: currentUser } = useUser(); const { user: currentUser } = useUser();
const [visible, toggleVisible] = useToggle(false); const [visible, toggleVisible] = useToggle(false);
const [collaborationUsers, setCollaborationUsers] = useState([]); const [collaborationUsers, setCollaborationUsers] = useState([]);
const content = useMemo( const content = useMemo(
() => ( () =>
!visible ? null : (
<div style={{ padding: '24px 0' }}> <div style={{ padding: '24px 0' }}>
<Members <Members
id={documentId} id={documentId}
@ -35,8 +47,9 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
/> />
</div> </div>
), ),
[documentId] [visible, documentId]
); );
const btn = useMemo( const btn = useMemo(
() => ( () => (
<Button theme="borderless" type="tertiary" disabled={disabled} icon={<IconUserAdd />} onClick={toggleVisible} /> <Button theme="borderless" type="tertiary" disabled={disabled} icon={<IconUserAdd />} onClick={toggleVisible} />
@ -113,6 +126,7 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
); );
})} })}
</AvatarGroup> </AvatarGroup>
{isMobile ? ( {isMobile ? (
<> <>
<Modal <Modal
@ -121,7 +135,7 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
visible={visible} visible={visible}
footer={null} footer={null}
onCancel={toggleVisible} onCancel={toggleVisible}
style={{ maxWidth: '96vw', maxHeight: '60vh', overflow: 'auto' }} style={mobileContainerStyle}
> >
{content} {content}
</Modal> </Modal>
@ -134,19 +148,7 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
onVisibleChange={toggleVisible} onVisibleChange={toggleVisible}
trigger="click" trigger="click"
position="bottomRight" position="bottomRight"
content={ content={<div style={pcContainerStyle}>{content}</div>}
<div
style={{
width: 412,
maxWidth: '96vw',
padding: '0 24px',
maxHeight: '60vh',
overflow: 'auto',
}}
>
{content}
</div>
}
> >
{btn} {btn}
</Dropdown> </Dropdown>

View File

@ -23,6 +23,7 @@ export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, author
if (!editor) return; if (!editor) return;
editor.commands.setContent(data); editor.commands.setContent(data);
}; };
event.on(USE_DOCUMENT_VERSION, handler); event.on(USE_DOCUMENT_VERSION, handler);
return () => { return () => {

View File

@ -28,6 +28,14 @@ interface IProps {
documentId: string; documentId: string;
} }
const ErrorContent = () => {
return (
<div style={{ margin: '10vh', textAlign: 'center' }}>
<SecureDocumentIllustration />
</div>
);
};
export const DocumentEditor: React.FC<IProps> = ({ documentId }) => { export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
const { isMobile } = IsOnMobile.useHook(); const { isMobile } = IsOnMobile.useHook();
const { width: windowWith } = useWindowSize(); const { width: windowWith } = useWindowSize();
@ -84,9 +92,7 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
mode="horizontal" mode="horizontal"
header={ header={
<> <>
<Tooltip content="返回" position="bottom">
<Button onMouseDown={goback} icon={<IconChevronLeft />} style={{ marginRight: 16 }} /> <Button onMouseDown={goback} icon={<IconChevronLeft />} style={{ marginRight: 16 }} />
</Tooltip>
<DataRender <DataRender
loading={docAuthLoading} loading={docAuthLoading}
error={docAuthError} error={docAuthError}
@ -125,13 +131,7 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
<DataRender <DataRender
loading={docAuthLoading} loading={docAuthLoading}
error={docAuthError} error={docAuthError}
errorContent={() => { errorContent={<ErrorContent />}
return (
<div style={{ margin: '10vh', textAlign: 'center' }}>
<SecureDocumentIllustration />
</div>
);
}}
normalContent={() => { normalContent={() => {
return ( return (
<> <>

View File

@ -64,7 +64,8 @@ export const EmojiPicker: React.FC<IProps> = ({ showClear = false, onSelectEmoji
}, [onSelectEmoji]); }, [onSelectEmoji]);
const content = useMemo( const content = useMemo(
() => ( () =>
!visible ? null : (
<div className={styles.wrap}> <div className={styles.wrap}>
<Tabs <Tabs
size="small" size="small"
@ -81,7 +82,12 @@ export const EmojiPicker: React.FC<IProps> = ({ showClear = false, onSelectEmoji
> >
{renderedList.map((list) => { {renderedList.map((list) => {
return ( return (
<TabPane key={list.title} tab={list.title} itemKey={list.title} style={{ height: 250, overflow: 'auto' }}> <TabPane
key={list.title}
tab={list.title}
itemKey={list.title}
style={{ height: 250, overflow: 'auto' }}
>
<div className={styles.listWrap}> <div className={styles.listWrap}>
{(list.data || []).map((ex) => ( {(list.data || []).map((ex) => (
<div key={ex} onClick={() => selectEmoji(ex)}> <div key={ex} onClick={() => selectEmoji(ex)}>
@ -95,7 +101,7 @@ export const EmojiPicker: React.FC<IProps> = ({ showClear = false, onSelectEmoji
</Tabs> </Tabs>
</div> </div>
), ),
[showClear, renderedList, selectEmoji, clear] [visible, showClear, renderedList, selectEmoji, clear]
); );
useEffect(() => { useEffect(() => {

View File

@ -8,7 +8,7 @@ import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import { EmptyBoxIllustration } from 'illustrations/empty-box'; import { EmptyBoxIllustration } from 'illustrations/empty-box';
import Link from 'next/link'; import Link from 'next/link';
import React, { useCallback } from 'react'; import React, { useCallback, useMemo } from 'react';
import styles from './index.module.scss'; import styles from './index.module.scss';
import { Placeholder } from './placeholder'; import { Placeholder } from './placeholder';
@ -17,19 +17,19 @@ const { Text } = Typography;
const PAGE_SIZE = 6; const PAGE_SIZE = 6;
const MessagesRender = ({ messageData, loading, error, onClick = null, page = 1, onPageChange = null }) => { const MessagesRender = ({ messageData, loading, error, onClick = null, page = 1, onPageChange = null }) => {
const total = (messageData && messageData.total) || 0; const [messages, total] = useMemo(
const messages = (messageData && messageData.data) || []; () => [(messageData && messageData.data) || [], (messageData && messageData.total) || 0],
[messageData]
);
const handleRead = (messageId) => { const handleRead = useCallback(
(messageId) => {
onClick && onClick(messageId); onClick && onClick(messageId);
}; },
[onClick]
);
return ( const renderNormalContent = useCallback(() => {
<DataRender
loading={loading}
loadingContent={<Placeholder />}
error={error}
normalContent={() => {
return ( return (
<div <div
className={styles.itemsWrap} className={styles.itemsWrap}
@ -81,8 +81,10 @@ const MessagesRender = ({ messageData, loading, error, onClick = null, page = 1,
)} )}
</div> </div>
); );
}} }, [handleRead, messages, onPageChange, page, total]);
/>
return (
<DataRender loading={loading} loadingContent={<Placeholder />} error={error} normalContent={renderNormalContent} />
); );
}; };
@ -106,20 +108,22 @@ const MessageBox = () => {
setPage: unreadSetPage, setPage: unreadSetPage,
} = useUnreadMessages(); } = useUnreadMessages();
const clearAll = () => { const clearAll = useCallback(() => {
Promise.all( Promise.all(
(unreadMsgs.data || []).map((msg) => { (unreadMsgs.data || []).map((msg) => {
return readMessage(msg.id); return readMessage(msg.id);
}) })
); );
}; }, [readMessage, unreadMsgs]);
const openModalOnMobile = useCallback(() => { const openModalOnMobile = useCallback(() => {
if (!isMobile) return; if (!isMobile) return;
toggleVisible(true); toggleVisible(true);
}, [isMobile, toggleVisible]); }, [isMobile, toggleVisible]);
const content = ( const content = useMemo(
() =>
visible ? (
<Tabs <Tabs
type="line" type="line"
size="small" size="small"
@ -160,6 +164,27 @@ const MessageBox = () => {
/> />
</TabPane> </TabPane>
</Tabs> </Tabs>
) : null,
[
allError,
allLoading,
allMsgs,
allPage,
allSetPage,
clearAll,
readError,
readLoading,
readMessage,
readMsgs,
readPage,
readSetPage,
unreadError,
unreadLoading,
unreadMsgs,
unreadPage,
unreadSetPage,
visible,
]
); );
const btn = ( const btn = (
@ -197,6 +222,8 @@ const MessageBox = () => {
</> </>
) : ( ) : (
<Dropdown <Dropdown
visible={visible}
onVisibleChange={toggleVisible}
position="bottomRight" position="bottomRight"
trigger="click" trigger="click"
content={<div style={{ width: 300, padding: '16px 16px 0' }}>{content}</div>} content={<div style={{ width: 300, padding: '16px 16px 0' }}>{content}</div>}
@ -210,5 +237,8 @@ const MessageBox = () => {
export const Message = () => { export const Message = () => {
const { loading, error } = useUser(); const { loading, error } = useUser();
return <DataRender loading={loading} error={error} normalContent={() => <MessageBox />} />;
const renderNormalContent = useCallback(() => <MessageBox />, []);
return <DataRender loading={loading} error={error} normalContent={renderNormalContent} />;
}; };

View File

@ -11,6 +11,9 @@ interface IProps {
onOk: (arg: ISize) => void; onOk: (arg: ISize) => void;
} }
const containerStyle = { padding: '0 12px 12px' };
const inlineBlockStyle = { display: 'inline-block' };
export const SizeSetter: React.FC<IProps> = ({ width, maxWidth, height, onOk, children }) => { export const SizeSetter: React.FC<IProps> = ({ width, maxWidth, height, onOk, children }) => {
const $form = useRef<FormApi>(); const $form = useRef<FormApi>();
@ -27,7 +30,7 @@ export const SizeSetter: React.FC<IProps> = ({ width, maxWidth, height, onOk, ch
position={'bottomLeft'} position={'bottomLeft'}
spacing={10} spacing={10}
render={ render={
<div style={{ padding: '0 12px 12px' }}> <div style={containerStyle}>
<Form initValues={{ width, height }} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left"> <Form initValues={{ width, height }} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left">
<Form.Input autofocus label="宽" field="width" {...(maxWidth ? { max: maxWidth } : {})} /> <Form.Input autofocus label="宽" field="width" {...(maxWidth ? { max: maxWidth } : {})} />
<Form.Input label="高" field="height" /> <Form.Input label="高" field="height" />
@ -38,7 +41,7 @@ export const SizeSetter: React.FC<IProps> = ({ width, maxWidth, height, onOk, ch
</div> </div>
} }
> >
<span style={{ display: 'inline-block' }}>{children}</span> <span style={inlineBlockStyle}>{children}</span>
</Dropdown> </Dropdown>
); );
}; };

View File

@ -136,6 +136,7 @@ export const _Tree = ({ data, docAsLink, getDocLink, isShareMode = false, needAd
defaultExpandedKeys={expandedKeys} defaultExpandedKeys={expandedKeys}
expandedKeys={expandedKeys} expandedKeys={expandedKeys}
onExpand={setExpandedKeys} onExpand={setExpandedKeys}
motion={false}
/> />
{needAddDocument && <AddDocument />} {needAddDocument && <AddDocument />}
</div> </div>

View File

@ -16,6 +16,8 @@ interface IProps {
rightNode: React.ReactNode; rightNode: React.ReactNode;
} }
const style = { width: '100%', height: '100%' };
export const AppDoubleColumnLayout: React.FC<IProps> = ({ leftNode, rightNode }) => { export const AppDoubleColumnLayout: React.FC<IProps> = ({ leftNode, rightNode }) => {
const { minWidth, maxWidth, width, isCollapsed, updateWidth, toggleCollapsed } = useDragableWidth(); const { minWidth, maxWidth, width, isCollapsed, updateWidth, toggleCollapsed } = useDragableWidth();
const debounceUpdate = useMemo(() => throttle(updateWidth, 200), [updateWidth]); const debounceUpdate = useMemo(() => throttle(updateWidth, 200), [updateWidth]);
@ -25,7 +27,7 @@ export const AppDoubleColumnLayout: React.FC<IProps> = ({ leftNode, rightNode })
<AppRouterHeader /> <AppRouterHeader />
<SemiLayout className={styles.contentWrap}> <SemiLayout className={styles.contentWrap}>
<SplitPane minSize={minWidth} maxSize={maxWidth} size={width} onChange={debounceUpdate}> <SplitPane minSize={minWidth} maxSize={maxWidth} size={width} onChange={debounceUpdate}>
<Sider style={{ width: '100%', height: '100%' }} className={styles.leftWrap}> <Sider style={style} className={styles.leftWrap}>
<Button <Button
size="small" size="small"
icon={isCollapsed ? <IconChevronRight /> : <IconChevronLeft />} icon={isCollapsed ? <IconChevronRight /> : <IconChevronLeft />}

View File

@ -9,7 +9,7 @@ import { useRecentDocuments } from 'data/document';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import styles from './index.module.scss'; import styles from './index.module.scss';
import { Placeholder } from './placeholder'; import { Placeholder } from './placeholder';
@ -20,20 +20,7 @@ export const RecentDocs = ({ visible }) => {
const { query } = useRouter(); const { query } = useRouter();
const { data: recentDocs, loading, error, refresh } = useRecentDocuments(query.organizationId); const { data: recentDocs, loading, error, refresh } = useRecentDocuments(query.organizationId);
useEffect(() => { const renderNormalContent = useCallback(() => {
if (visible) {
refresh();
}
}, [visible, refresh]);
return (
<Tabs type="line" size="small">
<TabPane tab="文档" itemKey="docs">
<DataRender
loading={loading}
loadingContent={<Placeholder />}
error={error}
normalContent={() => {
return ( return (
<div className={styles.itemsWrap} style={{ margin: '0 -16px' }}> <div className={styles.itemsWrap} style={{ margin: '0 -16px' }}>
{recentDocs && recentDocs.length ? ( {recentDocs && recentDocs.length ? (
@ -65,11 +52,7 @@ export const RecentDocs = ({ visible }) => {
</div> </div>
</div> </div>
<div className={styles.rightWrap}> <div className={styles.rightWrap}>
<DocumentStar <DocumentStar organizationId={doc.organizationId} wikiId={doc.wikiId} documentId={doc.id} />
organizationId={doc.organizationId}
wikiId={doc.wikiId}
documentId={doc.id}
/>
</div> </div>
</a> </a>
</Link> </Link>
@ -81,7 +64,22 @@ export const RecentDocs = ({ visible }) => {
)} )}
</div> </div>
); );
}} }, [recentDocs]);
useEffect(() => {
if (visible) {
refresh();
}
}, [visible, refresh]);
return (
<Tabs type="line" size="small">
<TabPane tab="文档" itemKey="docs">
<DataRender
loading={loading}
loadingContent={<Placeholder />}
error={error}
normalContent={renderNormalContent}
/> />
</TabPane> </TabPane>
</Tabs> </Tabs>
@ -109,6 +107,8 @@ export const RecentMobileTrigger = ({ toggleVisible }) => {
return <span onClick={toggleVisible}></span>; return <span onClick={toggleVisible}></span>;
}; };
const dropdownContainerStyle = { width: 300, padding: '16px 16px 0' };
export const Recent = () => { export const Recent = () => {
const [visible, toggleVisible] = useToggle(false); const [visible, toggleVisible] = useToggle(false);
@ -120,7 +120,7 @@ export const Recent = () => {
visible={visible} visible={visible}
onVisibleChange={toggleVisible} onVisibleChange={toggleVisible}
content={ content={
<div style={{ width: 300, padding: '16px 16px 0' }}> <div style={dropdownContainerStyle}>
<RecentDocs visible={visible} /> <RecentDocs visible={visible} />
</div> </div>
} }

View File

@ -7,7 +7,7 @@ import { useStarWikisInOrganization } from 'data/star';
import { useWikiDetail } from 'data/wiki'; import { useWikiDetail } from 'data/wiki';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React, { useCallback } from 'react';
import styles from './index.module.scss'; import styles from './index.module.scss';
import { Placeholder } from './placeholder'; import { Placeholder } from './placeholder';
@ -24,6 +24,58 @@ const WikiContent = () => {
} = useStarWikisInOrganization(query.organizationId); } = useStarWikisInOrganization(query.organizationId);
const { data: currentWiki } = useWikiDetail(query.wikiId); const { data: currentWiki } = useWikiDetail(query.wikiId);
const renderNormalContent = useCallback(() => {
return (
<div className={styles.itemsWrap}>
{starWikis && starWikis.length ? (
starWikis.map((wiki) => {
return (
<div className={styles.itemWrap} key={wiki.id}>
<Link
href={{
pathname: '/app/org/[organizationId]/wiki/[wikiId]',
query: {
organizationId: wiki.organizationId,
wikiId: wiki.id,
},
}}
>
<a className={styles.item}>
<div className={styles.leftWrap}>
<Avatar
shape="square"
size="small"
src={wiki.avatar}
style={{
marginRight: 8,
width: 24,
height: 24,
borderRadius: 4,
}}
>
{wiki.name.charAt(0)}
</Avatar>
<div>
<Text ellipsis={{ showTooltip: true }} style={{ width: 180 }}>
{wiki.name}
</Text>
</div>
</div>
<div className={styles.rightWrap}>
<WikiStar organizationId={wiki.organizationId} wikiId={wiki.id} onChange={refreshStarWikis} />
</div>
</a>
</Link>
</div>
);
})
) : (
<Empty message="收藏的知识库会出现在此处" />
)}
</div>
);
}, [refreshStarWikis, starWikis]);
return ( return (
<> <>
{currentWiki && ( {currentWiki && (
@ -85,61 +137,7 @@ const WikiContent = () => {
loading={loading} loading={loading}
loadingContent={<Placeholder />} loadingContent={<Placeholder />}
error={error} error={error}
normalContent={() => { normalContent={renderNormalContent}
return (
<div className={styles.itemsWrap}>
{starWikis && starWikis.length ? (
starWikis.map((wiki) => {
return (
<div className={styles.itemWrap} key={wiki.id}>
<Link
href={{
pathname: '/app/org/[organizationId]/wiki/[wikiId]',
query: {
organizationId: wiki.organizationId,
wikiId: wiki.id,
},
}}
>
<a className={styles.item}>
<div className={styles.leftWrap}>
<Avatar
shape="square"
size="small"
src={wiki.avatar}
style={{
marginRight: 8,
width: 24,
height: 24,
borderRadius: 4,
}}
>
{wiki.name.charAt(0)}
</Avatar>
<div>
<Text ellipsis={{ showTooltip: true }} style={{ width: 180 }}>
{wiki.name}
</Text>
</div>
</div>
<div className={styles.rightWrap}>
<WikiStar
organizationId={wiki.organizationId}
wikiId={wiki.id}
onChange={refreshStarWikis}
/>
</div>
</a>
</Link>
</div>
);
})
) : (
<Empty message="收藏的知识库会出现在此处" />
)}
</div>
);
}}
/> />
<Dropdown.Divider /> <Dropdown.Divider />
<div className={styles.itemWrap}> <div className={styles.itemWrap}>

View File

@ -6,17 +6,17 @@ import styles from './index.module.scss';
const { Content } = SemiLayout; const { Content } = SemiLayout;
const style = {
padding: '16px 24px',
backgroundColor: 'var(--semi-color-bg-0)',
};
export const AppSingleColumnLayout: React.FC = ({ children }) => { export const AppSingleColumnLayout: React.FC = ({ children }) => {
return ( return (
<SemiLayout className={styles.wrap}> <SemiLayout className={styles.wrap}>
<AppRouterHeader /> <AppRouterHeader />
<SemiLayout className={styles.contentWrap}> <SemiLayout className={styles.contentWrap}>
<Content <Content style={style}>
style={{
padding: '16px 24px',
backgroundColor: 'var(--semi-color-bg-0)',
}}
>
<div>{children}</div> <div>{children}</div>
</Content> </Content>
</SemiLayout> </SemiLayout>

View File

@ -16,6 +16,8 @@ interface IProps {
rightNode: React.ReactNode; rightNode: React.ReactNode;
} }
const style = { width: '100%', height: '100%' };
export const DoubleColumnLayout: React.FC<IProps> = ({ leftNode, rightNode }) => { export const DoubleColumnLayout: React.FC<IProps> = ({ leftNode, rightNode }) => {
const { minWidth, maxWidth, width, isCollapsed, updateWidth, toggleCollapsed } = useDragableWidth(); const { minWidth, maxWidth, width, isCollapsed, updateWidth, toggleCollapsed } = useDragableWidth();
const debounceUpdate = useMemo(() => throttle(updateWidth, 200), [updateWidth]); const debounceUpdate = useMemo(() => throttle(updateWidth, 200), [updateWidth]);
@ -25,7 +27,7 @@ export const DoubleColumnLayout: React.FC<IProps> = ({ leftNode, rightNode }) =>
<RouterHeader /> <RouterHeader />
<SemiLayout className={styles.contentWrap}> <SemiLayout className={styles.contentWrap}>
<SplitPane minSize={minWidth} maxSize={maxWidth} size={width} onChange={debounceUpdate}> <SplitPane minSize={minWidth} maxSize={maxWidth} size={width} onChange={debounceUpdate}>
<Sider style={{ width: '100%', height: '100%' }} className={styles.leftWrap}> <Sider style={style} className={styles.leftWrap}>
<Button <Button
size="small" size="small"
icon={isCollapsed ? <IconChevronRight /> : <IconChevronLeft />} icon={isCollapsed ? <IconChevronRight /> : <IconChevronLeft />}

View File

@ -15,6 +15,8 @@ interface IProps {
rightNode: React.ReactNode; rightNode: React.ReactNode;
} }
const style = { width: '100%', height: '100%' };
export const PublicDoubleColumnLayout: React.FC<IProps> = ({ leftNode, rightNode }) => { export const PublicDoubleColumnLayout: React.FC<IProps> = ({ leftNode, rightNode }) => {
const { minWidth, maxWidth, width, isCollapsed, updateWidth, toggleCollapsed } = useDragableWidth(); const { minWidth, maxWidth, width, isCollapsed, updateWidth, toggleCollapsed } = useDragableWidth();
const debounceUpdate = useMemo(() => throttle(updateWidth, 200), [updateWidth]); const debounceUpdate = useMemo(() => throttle(updateWidth, 200), [updateWidth]);
@ -22,7 +24,7 @@ export const PublicDoubleColumnLayout: React.FC<IProps> = ({ leftNode, rightNode
return ( return (
<SemiLayout className={styles.wrap}> <SemiLayout className={styles.wrap}>
<SplitPane minSize={minWidth} maxSize={maxWidth} size={width} onChange={debounceUpdate}> <SplitPane minSize={minWidth} maxSize={maxWidth} size={width} onChange={debounceUpdate}>
<Sider style={{ width: '100%', height: '100%' }} className={styles.leftWrap}> <Sider style={style} className={styles.leftWrap}>
<Button <Button
size="small" size="small"
icon={isCollapsed ? <IconChevronRight /> : <IconChevronLeft />} icon={isCollapsed ? <IconChevronRight /> : <IconChevronLeft />}

View File

@ -6,17 +6,17 @@ import styles from './index.module.scss';
const { Content } = SemiLayout; const { Content } = SemiLayout;
const style = {
padding: '16px 24px',
backgroundColor: 'var(--semi-color-bg-0)',
};
export const SingleColumnLayout: React.FC = ({ children }) => { export const SingleColumnLayout: React.FC = ({ children }) => {
return ( return (
<SemiLayout className={styles.wrap}> <SemiLayout className={styles.wrap}>
<RouterHeader /> <RouterHeader />
<SemiLayout className={styles.contentWrap}> <SemiLayout className={styles.contentWrap}>
<Content <Content style={style}>
style={{
padding: '16px 24px',
backgroundColor: 'var(--semi-color-bg-0)',
}}
>
<div>{children}</div> <div>{children}</div>
</Content> </Content>
</SemiLayout> </SemiLayout>

View File

@ -132,7 +132,10 @@ export class BubbleMenuView {
trigger: 'manual', trigger: 'manual',
placement: 'top', placement: 'top',
hideOnClick: 'toggle', hideOnClick: 'toggle',
...Object.assign({ zIndex: 999 }, this.tippyOptions), ...Object.assign(
{ zIndex: 999, duration: 200, animation: 'shift-toward-subtle', moveTransition: 'transform 0.2s ease-in-out' },
this.tippyOptions
),
}); });
// maybe we have to hide tippy on its own blur event as well // maybe we have to hide tippy on its own blur event as well

View File

@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useState } from 'react';
import { BubbleMenuPlugin, BubbleMenuPluginProps } from './bubble-menu-plugin'; import { BubbleMenuPlugin, BubbleMenuPluginProps } from './bubble-menu-plugin';
@ -9,11 +9,9 @@ export type BubbleMenuProps = Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>,
}; };
export const BubbleMenu: React.FC<BubbleMenuProps> = (props) => { export const BubbleMenu: React.FC<BubbleMenuProps> = (props) => {
const $element = useRef<HTMLDivElement | null>(null); const [element, setElement] = useState<HTMLDivElement | null>(null);
useEffect(() => { useEffect(() => {
const element = $element.current;
if (!element) { if (!element) {
return; return;
} }
@ -46,10 +44,10 @@ export const BubbleMenu: React.FC<BubbleMenuProps> = (props) => {
editor.registerPlugin(plugin); editor.registerPlugin(plugin);
return () => editor.unregisterPlugin(pluginKey); return () => editor.unregisterPlugin(pluginKey);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.editor]); }, [props.editor, element]);
return ( return (
<div ref={$element} className={props.className} style={{ visibility: 'hidden' }}> <div ref={setElement} className={props.className} style={{ visibility: 'hidden' }}>
{props.children} {props.children}
</div> </div>
); );

View File

@ -63,7 +63,7 @@ export const COMMANDS: ICommand[] = [
label: '表格', label: '表格',
custom: (editor, runCommand) => ( custom: (editor, runCommand) => (
<Popover <Popover
key="table" key="custom-table"
showArrow showArrow
position="rightTop" position="rightTop"
zIndex={10000} zIndex={10000}
@ -93,7 +93,7 @@ export const COMMANDS: ICommand[] = [
label: '布局', label: '布局',
custom: (editor, runCommand) => ( custom: (editor, runCommand) => (
<Popover <Popover
key="table" key="custom-columns"
showArrow showArrow
position="rightTop" position="rightTop"
zIndex={10000} zIndex={10000}

View File

@ -47,35 +47,8 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
const copyMe = useCallback(() => copyNode(DocumentReference.name, editor), [editor]); const copyMe = useCallback(() => copyNode(DocumentReference.name, editor), [editor]);
const deleteMe = useCallback(() => deleteNode(DocumentReference.name, editor), [editor]); const deleteMe = useCallback(() => deleteNode(DocumentReference.name, editor), [editor]);
useEffect(() => { const renderNormalContent = useCallback(
if (defaultShowPicker && user && createUser === user.id) { () => (
toggleVisible(true);
editor.chain().updateAttributes(DocumentReference.name, { defaultShowPicker: false }).focus().run();
}
}, [editor, defaultShowPicker, toggleVisible, createUser, user]);
return (
<BubbleMenu
className={'bubble-menu'}
editor={editor}
pluginKey="document-reference-bubble-menu"
shouldShow={shouldShow}
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
>
<Space spacing={4}>
<Tooltip content="复制">
<Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
</Tooltip>
<Popover
spacing={15}
visible={visible}
onVisibleChange={toggleVisible}
content={
<DataRender
loading={loading}
error={error}
normalContent={() => (
<List <List
size="small" size="small"
style={{ maxHeight: 320, overflow: 'auto' }} style={{ maxHeight: 320, overflow: 'auto' }}
@ -100,9 +73,35 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
/> />
)} )}
/> />
)} ),
/> [selectDoc, tocs]
);
useEffect(() => {
if (defaultShowPicker && user && createUser === user.id) {
toggleVisible(true);
editor.chain().updateAttributes(DocumentReference.name, { defaultShowPicker: false }).focus().run();
} }
}, [editor, defaultShowPicker, toggleVisible, createUser, user]);
return (
<BubbleMenu
className={'bubble-menu'}
editor={editor}
pluginKey="document-reference-bubble-menu"
shouldShow={shouldShow}
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
>
<Space spacing={4}>
<Tooltip content="复制">
<Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
</Tooltip>
<Popover
spacing={15}
visible={visible}
onVisibleChange={toggleVisible}
content={<DataRender loading={loading} error={error} normalContent={renderNormalContent} />}
trigger="click" trigger="click"
position="bottomLeft" position="bottomLeft"
showArrow showArrow

View File

@ -4,6 +4,14 @@ import { Editor } from 'tiptap/core';
import { Title } from 'tiptap/core/extensions/title'; import { Title } from 'tiptap/core/extensions/title';
import { useActive } from 'tiptap/core/hooks/use-active'; import { useActive } from 'tiptap/core/hooks/use-active';
const containerStyle = { width: 90, marginRight: 10 };
const h1Style = { margin: 0, fontSize: '1.3em' };
const h2Style = { margin: 0, fontSize: '1.1em' };
const h3Style = { margin: 0, fontSize: '1.0em' };
const h4Style = { margin: 0, fontSize: '0.9em' };
const h5Style = { margin: 0, fontSize: '0.8em' };
const h6Style = { margin: 0, fontSize: '0.8em' };
export const Heading: React.FC<{ editor: Editor }> = ({ editor }) => { export const Heading: React.FC<{ editor: Editor }> = ({ editor }) => {
const isTitleActive = useActive(editor, Title.name); const isTitleActive = useActive(editor, Title.name);
const isH1 = useActive(editor, 'heading', { level: 1 }); const isH1 = useActive(editor, 'heading', { level: 1 });
@ -12,6 +20,7 @@ export const Heading: React.FC<{ editor: Editor }> = ({ editor }) => {
const isH4 = useActive(editor, 'heading', { level: 4 }); const isH4 = useActive(editor, 'heading', { level: 4 });
const isH5 = useActive(editor, 'heading', { level: 5 }); const isH5 = useActive(editor, 'heading', { level: 5 });
const isH6 = useActive(editor, 'heading', { level: 6 }); const isH6 = useActive(editor, 'heading', { level: 6 });
const current = useMemo(() => { const current = useMemo(() => {
if (isH1) return 1; if (isH1) return 1;
if (isH2) return 2; if (isH2) return 2;
@ -34,25 +43,25 @@ export const Heading: React.FC<{ editor: Editor }> = ({ editor }) => {
); );
return ( return (
<Select disabled={isTitleActive} value={current} onChange={toggle} style={{ width: 90, marginRight: 10 }}> <Select disabled={isTitleActive} value={current} onChange={toggle} style={containerStyle}>
<Select.Option value="paragraph"></Select.Option> <Select.Option value="paragraph"></Select.Option>
<Select.Option value={1}> <Select.Option value={1}>
<h1 style={{ margin: 0, fontSize: '1.3em' }}>1</h1> <h1 style={h1Style}>1</h1>
</Select.Option> </Select.Option>
<Select.Option value={2}> <Select.Option value={2}>
<h2 style={{ margin: 0, fontSize: '1.1em' }}>2</h2> <h2 style={h2Style}>2</h2>
</Select.Option> </Select.Option>
<Select.Option value={3}> <Select.Option value={3}>
<h3 style={{ margin: 0, fontSize: '1.0em' }}>3</h3> <h3 style={h3Style}>3</h3>
</Select.Option> </Select.Option>
<Select.Option value={4}> <Select.Option value={4}>
<h4 style={{ margin: 0, fontSize: '0.9em' }}>4</h4> <h4 style={h4Style}>4</h4>
</Select.Option> </Select.Option>
<Select.Option value={5}> <Select.Option value={5}>
<h5 style={{ margin: 0, fontSize: '0.8em' }}>5</h5> <h5 style={h5Style}>5</h5>
</Select.Option> </Select.Option>
<Select.Option value={6}> <Select.Option value={6}>
<h6 style={{ margin: 0, fontSize: '0.8em' }}>6</h6> <h6 style={h6Style}>6</h6>
</Select.Option> </Select.Option>
</Select> </Select>
); );

View File

@ -10,6 +10,29 @@ import { useActive } from 'tiptap/core/hooks/use-active';
import { COMMANDS, insertMenuLRUCache, transformToCommands } from '../commands'; import { COMMANDS, insertMenuLRUCache, transformToCommands } from '../commands';
const _CommandRender = ({ commands, editor, runCommand }) => {
return (
<Dropdown.Menu>
{commands.map((command, index) => {
return command.title ? (
<Dropdown.Title key={'title' + index}>{command.title}</Dropdown.Title>
) : command.custom ? (
command.custom(editor, runCommand)
) : (
<Dropdown.Item key={index + '-' + command.label} onClick={runCommand(command)}>
{command.icon}
{command.label}
</Dropdown.Item>
);
})}
</Dropdown.Menu>
);
};
const CommandRender = React.memo(_CommandRender, (prevProps, nextProps) => {
return prevProps.commands.length === nextProps.commands.length;
});
export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => { export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
const { user } = useUser(); const { user } = useUser();
const [recentUsed, setRecentUsed] = useState([]); const [recentUsed, setRecentUsed] = useState([]);
@ -19,7 +42,7 @@ export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
const renderedCommands = useMemo( const renderedCommands = useMemo(
() => () =>
(recentUsed.length ? [{ title: '最近使用' }, ...recentUsed, ...COMMANDS] : COMMANDS).filter((command) => { (recentUsed.length ? [{ title: '最近使用' }, ...recentUsed, ...COMMANDS] : COMMANDS).filter((command) => {
return command.label === '表格' || command.label === '布局' ? 'custom' in command : true; return command.label === '表格' || command.label === '布局' ? Boolean(command) : true;
}), }),
[recentUsed] [recentUsed]
); );
@ -55,22 +78,7 @@ export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
maxHeight: 'calc(90vh - 120px)', maxHeight: 'calc(90vh - 120px)',
overflowY: 'auto', overflowY: 'auto',
}} }}
render={ render={visible ? <CommandRender commands={renderedCommands} editor={editor} runCommand={runCommand} /> : null}
<Dropdown.Menu>
{renderedCommands.map((command, index) => {
return command.title ? (
<Dropdown.Title key={'title' + index}>{command.title}</Dropdown.Title>
) : command.custom ? (
command.custom(editor, runCommand)
) : (
<Dropdown.Item key={index + '-' + command.label} onClick={runCommand(command)}>
{command.icon}
{command.label}
</Dropdown.Item>
);
})}
</Dropdown.Menu>
}
> >
<div> <div>
<Tooltip content="插入"> <Tooltip content="插入">

View File

@ -1,6 +1,7 @@
import { Button, Input, Popover, SideSheet, Space, Typography } from '@douyinfe/semi-ui'; import { Button, Input, Popover, SideSheet, Space, Typography } from '@douyinfe/semi-ui';
import { IconSearchReplace } from 'components/icons'; import { IconSearchReplace } from 'components/icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import deepEqual from 'deep-equal';
import { IsOnMobile } from 'hooks/use-on-mobile'; import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
@ -9,6 +10,9 @@ import { ON_SEARCH_RESULTS, SearchNReplace } from 'tiptap/core/extensions/search
const { Text } = Typography; const { Text } = Typography;
const headerStyle: React.CSSProperties = { borderBottom: '1px solid var(--semi-color-border)' };
const marginBottomStyle: React.CSSProperties = { marginBottom: 12 };
export const Search: React.FC<{ editor: Editor }> = ({ editor }) => { export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
const { isMobile } = IsOnMobile.useHook(); const { isMobile } = IsOnMobile.useHook();
const [visible, toggleVisible] = useToggle(false); const [visible, toggleVisible] = useToggle(false);
@ -32,16 +36,18 @@ export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
}, [visible]); }, [visible]);
useEffect(() => { useEffect(() => {
if (!visible) return;
if (editor && editor.commands && editor.commands.setSearchTerm) { if (editor && editor.commands && editor.commands.setSearchTerm) {
editor.commands.setSearchTerm(searchValue); editor.commands.setSearchTerm(searchValue);
} }
}, [searchValue, editor]); }, [visible, searchValue, editor]);
useEffect(() => { useEffect(() => {
if (!visible) return;
if (editor && editor.commands && editor.commands.setReplaceTerm) { if (editor && editor.commands && editor.commands.setReplaceTerm) {
editor.commands.setReplaceTerm(replaceValue); editor.commands.setReplaceTerm(replaceValue);
} }
}, [replaceValue, editor]); }, [visible, replaceValue, editor]);
useEffect(() => { useEffect(() => {
if (!editor) return; if (!editor) return;
@ -51,10 +57,12 @@ export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!searchExtension) return; if (!searchExtension) return;
const listener = () => { const listener = () => {
if (!visible) return;
const currentIndex = searchExtension ? searchExtension.options.currentIndex : -1; const currentIndex = searchExtension ? searchExtension.options.currentIndex : -1;
const results = searchExtension ? searchExtension.options.results : []; const results = searchExtension ? searchExtension.options.results : [];
setCurrentIndex(currentIndex); setCurrentIndex((preIndex) => (preIndex !== currentIndex ? currentIndex : preIndex));
setResults(results); setResults((prevResults) => (deepEqual(prevResults, results) ? prevResults : results));
}; };
editor.eventEmitter && editor.eventEmitter.on(ON_SEARCH_RESULTS, listener); editor.eventEmitter && editor.eventEmitter.on(ON_SEARCH_RESULTS, listener);
@ -63,11 +71,11 @@ export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!searchExtension) return; if (!searchExtension) return;
editor.eventEmitter && editor.eventEmitter.off(ON_SEARCH_RESULTS, listener); editor.eventEmitter && editor.eventEmitter.off(ON_SEARCH_RESULTS, listener);
}; };
}, [editor]); }, [visible, editor]);
const content = ( const content = (
<div style={{ padding: isMobile ? '24px 0' : 0 }}> <div style={{ padding: isMobile ? '24px 0' : 0 }}>
<div style={{ marginBottom: 12 }}> <div style={marginBottomStyle}>
<Text type="tertiary"></Text> <Text type="tertiary"></Text>
<Input <Input
autofocus autofocus
@ -76,7 +84,7 @@ export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
suffix={results.length ? `${currentIndex + 1}/${results.length}` : ''} suffix={results.length ? `${currentIndex + 1}/${results.length}` : ''}
/> />
</div> </div>
<div style={{ marginBottom: 12 }}> <div style={marginBottomStyle}>
<Text type="tertiary"></Text> <Text type="tertiary"></Text>
<Input value={replaceValue} onChange={setReplaceValue} /> <Input value={replaceValue} onChange={setReplaceValue} />
</div> </div>
@ -113,7 +121,7 @@ export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
{isMobile ? ( {isMobile ? (
<> <>
<SideSheet <SideSheet
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} headerStyle={headerStyle}
placement="bottom" placement="bottom"
title={'查找替换'} title={'查找替换'}
visible={visible} visible={visible}

View File

@ -138,17 +138,7 @@ export const CollaborationEditor = forwardRef((props: ICollaborationEditorProps,
return ( return (
<div className={styles.wrap}> <div className={styles.wrap}>
<DataRender <DataRender loading={loading} error={error} errorContent={renderError} normalContent={renderEditor} />
loading={loading}
loadingContent={
<div style={{ margin: 'auto' }}>
<Spin />
</div>
}
error={error}
errorContent={renderError}
normalContent={renderEditor}
/>
</div> </div>
); );
}); });