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,25 +89,26 @@ export const ColorPicker: React.FC<{
const [visible, toggleVisible] = useToggle(false);
const content = useMemo(
() => (
<div style={{ padding: isMobile ? '24px 0 24px' : '12px 16px', width: isMobile ? 'auto' : 272 }}>
<div className={styles.emptyWrap} onClick={() => onSetColor(null)}>
<span></span>
<Text></Text>
</div>
() =>
!visible ? null : (
<div style={{ padding: isMobile ? '24px 0 24px' : '12px 16px', width: isMobile ? 'auto' : 272 }}>
<div className={styles.emptyWrap} onClick={() => onSetColor(null)}>
<span></span>
<Text></Text>
</div>
<div className={styles.colorWrap}>
{colors.map((color) => {
return (
<div key={color} className={styles.colorItem} onClick={() => onSetColor(color)}>
<span style={{ backgroundColor: color }}></span>
</div>
);
})}
<div className={styles.colorWrap}>
{colors.map((color) => {
return (
<div key={color} className={styles.colorItem} onClick={() => onSetColor(color)}>
<span style={{ backgroundColor: color }}></span>
</div>
);
})}
</div>
</div>
</div>
),
[onSetColor, isMobile]
),
[onSetColor, isMobile, visible]
);
if (disabled) return <span style={{ display: 'inline-block' }}>{children}</span>;
@ -132,7 +133,14 @@ export const ColorPicker: React.FC<{
</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>
</Dropdown>
)}

View File

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

View File

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

View File

@ -16,27 +16,40 @@ interface IProps {
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 }) => {
const { isMobile } = IsOnMobile.useHook();
const toastedUsersRef = useRef([]);
const { user: currentUser } = useUser();
const [visible, toggleVisible] = useToggle(false);
const [collaborationUsers, setCollaborationUsers] = useState([]);
const content = useMemo(
() => (
<div style={{ padding: '24px 0' }}>
<Members
id={documentId}
hook={useDoumentMembers}
descriptions={[
'权限继承:默认继承知识库成员权限',
'超级管理员:组织超级管理员、知识库超级管理员和文档创建者',
]}
/>
</div>
),
[documentId]
() =>
!visible ? null : (
<div style={{ padding: '24px 0' }}>
<Members
id={documentId}
hook={useDoumentMembers}
descriptions={[
'权限继承:默认继承知识库成员权限',
'超级管理员:组织超级管理员、知识库超级管理员和文档创建者',
]}
/>
</div>
),
[visible, documentId]
);
const btn = useMemo(
() => (
<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>
{isMobile ? (
<>
<Modal
@ -121,7 +135,7 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
visible={visible}
footer={null}
onCancel={toggleVisible}
style={{ maxWidth: '96vw', maxHeight: '60vh', overflow: 'auto' }}
style={mobileContainerStyle}
>
{content}
</Modal>
@ -134,19 +148,7 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
onVisibleChange={toggleVisible}
trigger="click"
position="bottomRight"
content={
<div
style={{
width: 412,
maxWidth: '96vw',
padding: '0 24px',
maxHeight: '60vh',
overflow: 'auto',
}}
>
{content}
</div>
}
content={<div style={pcContainerStyle}>{content}</div>}
>
{btn}
</Dropdown>

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle';
import { EmptyBoxIllustration } from 'illustrations/empty-box';
import Link from 'next/link';
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import styles from './index.module.scss';
import { Placeholder } from './placeholder';
@ -17,72 +17,74 @@ const { Text } = Typography;
const PAGE_SIZE = 6;
const MessagesRender = ({ messageData, loading, error, onClick = null, page = 1, onPageChange = null }) => {
const total = (messageData && messageData.total) || 0;
const messages = (messageData && messageData.data) || [];
const [messages, total] = useMemo(
() => [(messageData && messageData.data) || [], (messageData && messageData.total) || 0],
[messageData]
);
const handleRead = (messageId) => {
onClick && onClick(messageId);
};
const handleRead = useCallback(
(messageId) => {
onClick && onClick(messageId);
},
[onClick]
);
const renderNormalContent = useCallback(() => {
return (
<div
className={styles.itemsWrap}
style={{ margin: '8px -16px', minHeight: 224 }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
{messages.length ? (
<>
{messages.map((msg) => {
return (
<div key={msg.id} className={styles.itemWrap} onClick={() => handleRead(msg.id)}>
<Link href={msg.url}>
<a className={styles.item}>
<div className={styles.leftWrap}>
<Text
ellipsis={{
showTooltip: {
opts: { content: msg.message },
},
}}
style={{ width: 240 }}
>
{msg.title}
</Text>
</div>
</a>
</Link>
</div>
);
})}
{total > PAGE_SIZE && (
<div className={styles.paginationWrap}>
<Pagination
size="small"
total={total}
currentPage={page}
pageSize={PAGE_SIZE}
style={{ textAlign: 'center' }}
onPageChange={onPageChange}
/>
</div>
)}
</>
) : (
<Empty illustration={<EmptyBoxIllustration />} message="暂无消息" />
)}
</div>
);
}, [handleRead, messages, onPageChange, page, total]);
return (
<DataRender
loading={loading}
loadingContent={<Placeholder />}
error={error}
normalContent={() => {
return (
<div
className={styles.itemsWrap}
style={{ margin: '8px -16px', minHeight: 224 }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
{messages.length ? (
<>
{messages.map((msg) => {
return (
<div key={msg.id} className={styles.itemWrap} onClick={() => handleRead(msg.id)}>
<Link href={msg.url}>
<a className={styles.item}>
<div className={styles.leftWrap}>
<Text
ellipsis={{
showTooltip: {
opts: { content: msg.message },
},
}}
style={{ width: 240 }}
>
{msg.title}
</Text>
</div>
</a>
</Link>
</div>
);
})}
{total > PAGE_SIZE && (
<div className={styles.paginationWrap}>
<Pagination
size="small"
total={total}
currentPage={page}
pageSize={PAGE_SIZE}
style={{ textAlign: 'center' }}
onPageChange={onPageChange}
/>
</div>
)}
</>
) : (
<Empty illustration={<EmptyBoxIllustration />} message="暂无消息" />
)}
</div>
);
}}
/>
<DataRender loading={loading} loadingContent={<Placeholder />} error={error} normalContent={renderNormalContent} />
);
};
@ -106,60 +108,83 @@ const MessageBox = () => {
setPage: unreadSetPage,
} = useUnreadMessages();
const clearAll = () => {
const clearAll = useCallback(() => {
Promise.all(
(unreadMsgs.data || []).map((msg) => {
return readMessage(msg.id);
})
);
};
}, [readMessage, unreadMsgs]);
const openModalOnMobile = useCallback(() => {
if (!isMobile) return;
toggleVisible(true);
}, [isMobile, toggleVisible]);
const content = (
<Tabs
type="line"
size="small"
tabBarExtraContent={
unreadMsgs && unreadMsgs.total > 0 ? (
<Text type="quaternary" onClick={clearAll} style={{ cursor: 'pointer' }}>
</Text>
) : null
}
>
<TabPane tab="未读" itemKey="unread">
<MessagesRender
messageData={unreadMsgs}
loading={unreadLoading}
error={unreadError}
onClick={readMessage}
page={unreadPage}
onPageChange={unreadSetPage}
/>
</TabPane>
<TabPane tab="已读" itemKey="read">
<MessagesRender
messageData={readMsgs}
loading={readLoading}
error={readError}
page={readPage}
onPageChange={readSetPage}
/>
</TabPane>
<TabPane tab="全部" itemKey="all">
<MessagesRender
messageData={allMsgs}
loading={allLoading}
error={allError}
page={allPage}
onPageChange={allSetPage}
/>
</TabPane>
</Tabs>
const content = useMemo(
() =>
visible ? (
<Tabs
type="line"
size="small"
tabBarExtraContent={
unreadMsgs && unreadMsgs.total > 0 ? (
<Text type="quaternary" onClick={clearAll} style={{ cursor: 'pointer' }}>
</Text>
) : null
}
>
<TabPane tab="未读" itemKey="unread">
<MessagesRender
messageData={unreadMsgs}
loading={unreadLoading}
error={unreadError}
onClick={readMessage}
page={unreadPage}
onPageChange={unreadSetPage}
/>
</TabPane>
<TabPane tab="已读" itemKey="read">
<MessagesRender
messageData={readMsgs}
loading={readLoading}
error={readError}
page={readPage}
onPageChange={readSetPage}
/>
</TabPane>
<TabPane tab="全部" itemKey="all">
<MessagesRender
messageData={allMsgs}
loading={allLoading}
error={allError}
page={allPage}
onPageChange={allSetPage}
/>
</TabPane>
</Tabs>
) : null,
[
allError,
allLoading,
allMsgs,
allPage,
allSetPage,
clearAll,
readError,
readLoading,
readMessage,
readMsgs,
readPage,
readSetPage,
unreadError,
unreadLoading,
unreadMsgs,
unreadPage,
unreadSetPage,
visible,
]
);
const btn = (
@ -197,6 +222,8 @@ const MessageBox = () => {
</>
) : (
<Dropdown
visible={visible}
onVisibleChange={toggleVisible}
position="bottomRight"
trigger="click"
content={<div style={{ width: 300, padding: '16px 16px 0' }}>{content}</div>}
@ -210,5 +237,8 @@ const MessageBox = () => {
export const Message = () => {
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;
}
const containerStyle = { padding: '0 12px 12px' };
const inlineBlockStyle = { display: 'inline-block' };
export const SizeSetter: React.FC<IProps> = ({ width, maxWidth, height, onOk, children }) => {
const $form = useRef<FormApi>();
@ -27,7 +30,7 @@ export const SizeSetter: React.FC<IProps> = ({ width, maxWidth, height, onOk, ch
position={'bottomLeft'}
spacing={10}
render={
<div style={{ padding: '0 12px 12px' }}>
<div style={containerStyle}>
<Form initValues={{ width, height }} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left">
<Form.Input autofocus label="宽" field="width" {...(maxWidth ? { max: maxWidth } : {})} />
<Form.Input label="高" field="height" />
@ -38,7 +41,7 @@ export const SizeSetter: React.FC<IProps> = ({ width, maxWidth, height, onOk, ch
</div>
}
>
<span style={{ display: 'inline-block' }}>{children}</span>
<span style={inlineBlockStyle}>{children}</span>
</Dropdown>
);
};

View File

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

View File

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

View File

@ -9,7 +9,7 @@ import { useRecentDocuments } from 'data/document';
import { useToggle } from 'hooks/use-toggle';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useEffect } from 'react';
import React, { useCallback, useEffect } from 'react';
import styles from './index.module.scss';
import { Placeholder } from './placeholder';
@ -20,6 +20,52 @@ export const RecentDocs = ({ visible }) => {
const { query } = useRouter();
const { data: recentDocs, loading, error, refresh } = useRecentDocuments(query.organizationId);
const renderNormalContent = useCallback(() => {
return (
<div className={styles.itemsWrap} style={{ margin: '0 -16px' }}>
{recentDocs && recentDocs.length ? (
recentDocs.map((doc) => {
return (
<div className={styles.itemWrap} key={doc.id}>
<Link
href={{
pathname: '/app/org/[organizationId]/wiki/[wikiId]/doc/[documentId]',
query: {
organizationId: doc.organizationId,
wikiId: doc.wikiId,
documentId: doc.id,
},
}}
>
<a className={styles.item}>
<div className={styles.leftWrap}>
<IconDocumentFill style={{ marginRight: 12 }} />
<div>
<Text ellipsis={{ showTooltip: true }} style={{ width: 180 }}>
{doc.title}
</Text>
<Text size="small" type="tertiary">
{doc.createUser && doc.createUser.name} <LocaleTime date={doc.updatedAt} />
</Text>
</div>
</div>
<div className={styles.rightWrap}>
<DocumentStar organizationId={doc.organizationId} wikiId={doc.wikiId} documentId={doc.id} />
</div>
</a>
</Link>
</div>
);
})
) : (
<Empty message="最近访问的文档会出现在此处" />
)}
</div>
);
}, [recentDocs]);
useEffect(() => {
if (visible) {
refresh();
@ -33,55 +79,7 @@ export const RecentDocs = ({ visible }) => {
loading={loading}
loadingContent={<Placeholder />}
error={error}
normalContent={() => {
return (
<div className={styles.itemsWrap} style={{ margin: '0 -16px' }}>
{recentDocs && recentDocs.length ? (
recentDocs.map((doc) => {
return (
<div className={styles.itemWrap} key={doc.id}>
<Link
href={{
pathname: '/app/org/[organizationId]/wiki/[wikiId]/doc/[documentId]',
query: {
organizationId: doc.organizationId,
wikiId: doc.wikiId,
documentId: doc.id,
},
}}
>
<a className={styles.item}>
<div className={styles.leftWrap}>
<IconDocumentFill style={{ marginRight: 12 }} />
<div>
<Text ellipsis={{ showTooltip: true }} style={{ width: 180 }}>
{doc.title}
</Text>
<Text size="small" type="tertiary">
{doc.createUser && doc.createUser.name} <LocaleTime date={doc.updatedAt} />
</Text>
</div>
</div>
<div className={styles.rightWrap}>
<DocumentStar
organizationId={doc.organizationId}
wikiId={doc.wikiId}
documentId={doc.id}
/>
</div>
</a>
</Link>
</div>
);
})
) : (
<Empty message="最近访问的文档会出现在此处" />
)}
</div>
);
}}
normalContent={renderNormalContent}
/>
</TabPane>
</Tabs>
@ -109,6 +107,8 @@ export const RecentMobileTrigger = ({ toggleVisible }) => {
return <span onClick={toggleVisible}></span>;
};
const dropdownContainerStyle = { width: 300, padding: '16px 16px 0' };
export const Recent = () => {
const [visible, toggleVisible] = useToggle(false);
@ -120,7 +120,7 @@ export const Recent = () => {
visible={visible}
onVisibleChange={toggleVisible}
content={
<div style={{ width: 300, padding: '16px 16px 0' }}>
<div style={dropdownContainerStyle}>
<RecentDocs visible={visible} />
</div>
}

View File

@ -7,7 +7,7 @@ import { useStarWikisInOrganization } from 'data/star';
import { useWikiDetail } from 'data/wiki';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
import React, { useCallback } from 'react';
import styles from './index.module.scss';
import { Placeholder } from './placeholder';
@ -24,6 +24,58 @@ const WikiContent = () => {
} = useStarWikisInOrganization(query.organizationId);
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 (
<>
{currentWiki && (
@ -85,61 +137,7 @@ const WikiContent = () => {
loading={loading}
loadingContent={<Placeholder />}
error={error}
normalContent={() => {
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>
);
}}
normalContent={renderNormalContent}
/>
<Dropdown.Divider />
<div className={styles.itemWrap}>

View File

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

View File

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

View File

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

View File

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

View File

@ -132,7 +132,10 @@ export class BubbleMenuView {
trigger: 'manual',
placement: 'top',
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

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

View File

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

View File

@ -47,6 +47,36 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
const copyMe = useCallback(() => copyNode(DocumentReference.name, editor), [editor]);
const deleteMe = useCallback(() => deleteNode(DocumentReference.name, editor), [editor]);
const renderNormalContent = useCallback(
() => (
<List
size="small"
style={{ maxHeight: 320, overflow: 'auto' }}
dataSource={tocs}
renderItem={(item) => (
<List.Item
onClick={() => selectDoc(item)}
style={{ cursor: 'pointer' }}
main={
<div style={{ display: 'flex', alignItems: 'center' }}>
<Text style={{ display: 'flex', alignItems: 'center' }}>
<IconDocument />
</Text>
<Text
ellipsis={{ showTooltip: { opts: { content: item.title, position: 'right' } } }}
style={{ width: 150, paddingLeft: 6 }}
>
{item.title}
</Text>
</div>
}
/>
)}
/>
),
[selectDoc, tocs]
);
useEffect(() => {
if (defaultShowPicker && user && createUser === user.id) {
toggleVisible(true);
@ -71,38 +101,7 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
spacing={15}
visible={visible}
onVisibleChange={toggleVisible}
content={
<DataRender
loading={loading}
error={error}
normalContent={() => (
<List
size="small"
style={{ maxHeight: 320, overflow: 'auto' }}
dataSource={tocs}
renderItem={(item) => (
<List.Item
onClick={() => selectDoc(item)}
style={{ cursor: 'pointer' }}
main={
<div style={{ display: 'flex', alignItems: 'center' }}>
<Text style={{ display: 'flex', alignItems: 'center' }}>
<IconDocument />
</Text>
<Text
ellipsis={{ showTooltip: { opts: { content: item.title, position: 'right' } } }}
style={{ width: 150, paddingLeft: 6 }}
>
{item.title}
</Text>
</div>
}
/>
)}
/>
)}
/>
}
content={<DataRender loading={loading} error={error} normalContent={renderNormalContent} />}
trigger="click"
position="bottomLeft"
showArrow

View File

@ -4,6 +4,14 @@ import { Editor } from 'tiptap/core';
import { Title } from 'tiptap/core/extensions/title';
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 }) => {
const isTitleActive = useActive(editor, Title.name);
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 isH5 = useActive(editor, 'heading', { level: 5 });
const isH6 = useActive(editor, 'heading', { level: 6 });
const current = useMemo(() => {
if (isH1) return 1;
if (isH2) return 2;
@ -34,25 +43,25 @@ export const Heading: React.FC<{ editor: Editor }> = ({ editor }) => {
);
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={1}>
<h1 style={{ margin: 0, fontSize: '1.3em' }}>1</h1>
<h1 style={h1Style}>1</h1>
</Select.Option>
<Select.Option value={2}>
<h2 style={{ margin: 0, fontSize: '1.1em' }}>2</h2>
<h2 style={h2Style}>2</h2>
</Select.Option>
<Select.Option value={3}>
<h3 style={{ margin: 0, fontSize: '1.0em' }}>3</h3>
<h3 style={h3Style}>3</h3>
</Select.Option>
<Select.Option value={4}>
<h4 style={{ margin: 0, fontSize: '0.9em' }}>4</h4>
<h4 style={h4Style}>4</h4>
</Select.Option>
<Select.Option value={5}>
<h5 style={{ margin: 0, fontSize: '0.8em' }}>5</h5>
<h5 style={h5Style}>5</h5>
</Select.Option>
<Select.Option value={6}>
<h6 style={{ margin: 0, fontSize: '0.8em' }}>6</h6>
<h6 style={h6Style}>6</h6>
</Select.Option>
</Select>
);

View File

@ -10,6 +10,29 @@ import { useActive } from 'tiptap/core/hooks/use-active';
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 }) => {
const { user } = useUser();
const [recentUsed, setRecentUsed] = useState([]);
@ -19,7 +42,7 @@ export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
const renderedCommands = useMemo(
() =>
(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]
);
@ -55,22 +78,7 @@ export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
maxHeight: 'calc(90vh - 120px)',
overflowY: 'auto',
}}
render={
<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>
}
render={visible ? <CommandRender commands={renderedCommands} editor={editor} runCommand={runCommand} /> : null}
>
<div>
<Tooltip content="插入">

View File

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

View File

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