refactor: improve mobile ux

This commit is contained in:
fantasticit 2022-05-04 14:50:58 +08:00
parent af358c1e04
commit ef61f1bdf3
33 changed files with 813 additions and 526 deletions

View File

@ -36,7 +36,7 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 0, '@typescript-eslint/explicit-module-boundary-types': 0,
'@typescript-eslint/ban-types': 0, '@typescript-eslint/ban-types': 0,
'react-hooks/rules-of-hooks': 2, 'react-hooks/rules-of-hooks': 2,
'react-hooks/exhaustive-deps': 1, 'react-hooks/exhaustive-deps': 2,
'react/prop-types': 0, 'react/prop-types': 0,
'testing-library/no-unnecessary-act': 0, 'testing-library/no-unnecessary-act': 0,
'react/react-in-jsx-scope': 0, 'react/react-in-jsx-scope': 0,

View File

@ -128,7 +128,6 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId })
visible={visible} visible={visible}
onOk={handleOk} onOk={handleOk}
onCancel={() => toggleVisible(false)} onCancel={() => toggleVisible(false)}
maskClosable={false}
style={{ maxWidth: '96vw' }} style={{ maxWidth: '96vw' }}
footer={null} footer={null}
> >

View File

@ -8,12 +8,14 @@
> header { > header {
position: relative; position: relative;
z-index: 110; z-index: 110;
height: 60px;
background-color: var(--semi-color-nav-bg); background-color: var(--semi-color-nav-bg);
user-select: none; user-select: none;
> div { .mobileToolbar {
padding: 12px 16px;
overflow: auto; overflow: auto;
text-align: center;
border-bottom: 1px solid var(--semi-color-border);
} }
} }

View File

@ -28,7 +28,7 @@ interface IProps {
} }
export const DocumentEditor: React.FC<IProps> = ({ documentId }) => { export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
const { width: windowWith } = useWindowSize(); const { width: windowWith, isMobile } = useWindowSize();
const { width, fontSize } = useDocumentStyle(); const { width, fontSize } = useDocumentStyle();
const editorWrapClassNames = useMemo(() => { const editorWrapClassNames = useMemo(() => {
return width === 'standardWidth' ? styles.isStandardWidth : styles.isFullWidth; return width === 'standardWidth' ? styles.isStandardWidth : styles.isFullWidth;
@ -44,6 +44,20 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
}); });
}, [document, documentId]); }, [document, documentId]);
const actions = (
<Space>
{document && authority.readable && (
<DocumentCollaboration key="collaboration" wikiId={document.wikiId} documentId={documentId} />
)}
<DocumentShare key="share" documentId={documentId} />
<DocumentVersion key="version" documentId={documentId} onSelect={triggerUseDocumentVersion} />
<DocumentStar key="star" documentId={documentId} />
<Popover key="style" zIndex={1061} position={isMobile ? 'topRight' : 'bottomLeft'} content={<DocumentStyle />}>
<Button icon={<IconArticle />} theme="borderless" type="tertiary" />
</Popover>
</Space>
);
useEffect(() => { useEffect(() => {
event.on(CHANGE_DOCUMENT_TITLE, setTitle); event.on(CHANGE_DOCUMENT_TITLE, setTitle);
@ -61,7 +75,7 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
header={ header={
<> <>
<Tooltip content="返回" position="bottom"> <Tooltip content="返回" position="bottom">
<Button onClick={goback} icon={<IconChevronLeft />} style={{ marginRight: 16 }} /> <Button onMouseDown={goback} icon={<IconChevronLeft />} style={{ marginRight: 16 }} />
</Tooltip> </Tooltip>
<DataRender <DataRender
loading={docAuthLoading} loading={docAuthLoading}
@ -83,22 +97,19 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
</> </>
} }
footer={ footer={
<Space> <>
{document && authority.readable && ( {isMobile ? null : (
<DocumentCollaboration key="collaboration" wikiId={document.wikiId} documentId={documentId} /> <>
{actions}
<Divider />
</>
)} )}
<DocumentShare key="share" documentId={documentId} />
<DocumentVersion key="version" documentId={documentId} onSelect={triggerUseDocumentVersion} />
<DocumentStar key="star" documentId={documentId} />
<Popover key="style" zIndex={1061} position="bottomLeft" content={<DocumentStyle />}>
<Button icon={<IconArticle />} theme="borderless" type="tertiary" />
</Popover>
<Theme /> <Theme />
<Divider />
<User /> <User />
</Space> </>
} }
/> />
{isMobile && <div className={styles.mobileToolbar}>{actions}</div>}
</header> </header>
<main className={styles.contentWrap}> <main className={styles.contentWrap}>
<DataRender <DataRender

View File

@ -38,4 +38,22 @@
border-top: 1px solid var(--semi-color-border); border-top: 1px solid var(--semi-color-border);
} }
} }
.mobileToolbar {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 999;
display: flex;
height: 49px;
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
background: var(--semi-color-bg-1);
box-sizing: content-box;
justify-content: space-around;
align-items: center;
border-top: 1px solid var(--semi-color-border);
}
} }

View File

@ -32,7 +32,8 @@ const EditBtnStyle = {
borderRadius: '100%', borderRadius: '100%',
backgroundColor: '#0077fa', backgroundColor: '#0077fa',
color: '#fff', color: '#fff',
bottom: 100, right: 16,
bottom: 70,
transform: 'translateY(-50px)', transform: 'translateY(-50px)',
}; };
@ -42,7 +43,7 @@ interface IProps {
export const DocumentReader: React.FC<IProps> = ({ documentId }) => { export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
const [container, setContainer] = useState<HTMLDivElement>(); const [container, setContainer] = useState<HTMLDivElement>();
const { width: windowWidth } = useWindowSize(); const { width: windowWidth, isMobile } = useWindowSize();
const { width, fontSize } = useDocumentStyle(); const { width, fontSize } = useDocumentStyle();
const editorWrapClassNames = useMemo(() => { const editorWrapClassNames = useMemo(() => {
return width === 'standardWidth' ? styles.isStandardWidth : styles.isFullWidth; return width === 'standardWidth' ? styles.isStandardWidth : styles.isFullWidth;
@ -70,6 +71,32 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
Router.push(`/wiki/${document.wikiId}/document/${document.id}/edit`); Router.push(`/wiki/${document.wikiId}/document/${document.id}/edit`);
}, [document]); }, [document]);
const actions = useMemo(
() => (
<Space>
{document && authority.readable && (
<DocumentCollaboration key="collaboration" wikiId={document.wikiId} documentId={documentId} />
)}
{authority && authority.editable && (
<Tooltip key="edit" content="编辑" position="bottom">
<Button icon={<IconEdit />} onMouseDown={gotoEdit} />
</Tooltip>
)}
{authority && authority.readable && (
<>
<DocumentShare key="share" documentId={documentId} />
<DocumentVersion key="version" documentId={documentId} />
<DocumentStar key="star" documentId={documentId} />
</>
)}
<Popover key="style" zIndex={1061} position={isMobile ? 'topRight' : 'bottomLeft'} content={<DocumentStyle />}>
<Button icon={<IconArticle />} theme="borderless" type="tertiary" />
</Popover>
</Space>
),
[document, documentId, authority, isMobile, gotoEdit]
);
if (!documentId) return null; if (!documentId) return null;
return ( return (
@ -89,35 +116,14 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
ellipsis={{ ellipsis={{
showTooltip: { opts: { content: document.title, style: { wordBreak: 'break-all' } } }, showTooltip: { opts: { content: document.title, style: { wordBreak: 'break-all' } } },
}} }}
style={{ width: ~~(windowWidth / 4) }} style={{ width: isMobile ? windowWidth - 100 : ~~(windowWidth / 4) }}
> >
{document.title} {document.title}
</Text> </Text>
)} )}
/> />
} }
footer={ footer={isMobile ? <></> : actions}
<Space>
{document && authority.readable && (
<DocumentCollaboration key="collaboration" wikiId={document.wikiId} documentId={documentId} />
)}
{authority && authority.editable && (
<Tooltip key="edit" content="编辑" position="bottom">
<Button icon={<IconEdit />} onClick={gotoEdit} />
</Tooltip>
)}
{authority && authority.readable && (
<>
<DocumentShare key="share" documentId={documentId} />
<DocumentVersion key="version" documentId={documentId} />
<DocumentStar key="star" documentId={documentId} />
</>
)}
<Popover key="style" zIndex={1061} position="bottomLeft" content={<DocumentStyle />}>
<Button icon={<IconArticle />} theme="borderless" type="tertiary" />
</Popover>
</Space>
}
></Nav> ></Nav>
</Header> </Header>
<Layout className={styles.contentWrap}> <Layout className={styles.contentWrap}>
@ -153,12 +159,12 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
<CommentEditor documentId={document.id} /> <CommentEditor documentId={document.id} />
</div> </div>
)} )}
{authority && authority.editable && container && ( {!isMobile && authority && authority.editable && container && (
<BackTop style={EditBtnStyle} onClick={gotoEdit} target={() => container} visibilityHeight={200}> <BackTop style={EditBtnStyle} onClick={gotoEdit} target={() => container} visibilityHeight={200}>
<IconEdit /> <IconEdit />
</BackTop> </BackTop>
)} )}
{container && <BackTop target={() => container} />} {container && <BackTop style={{ bottom: 65, right: 16 }} target={() => container} />}
</> </>
); );
}} }}
@ -166,6 +172,7 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
</div> </div>
</div> </div>
</Layout> </Layout>
{isMobile && <div className={styles.mobileToolbar}>{actions}</div>}
</div> </div>
); );
}; };

View File

@ -40,14 +40,12 @@ export const DocumentShare: React.FC<IProps> = ({ documentId, render }) => {
{isPublic ? '分享中' : '分享'} {isPublic ? '分享中' : '分享'}
</Button> </Button>
)} )}
<Modal <Modal
title={isPublic ? '关闭分享' : '开启分享'} title={isPublic ? '关闭分享' : '开启分享'}
okText={isPublic ? '关闭分享' : '开启分享'} okText={isPublic ? '关闭分享' : '开启分享'}
visible={visible} visible={visible}
onOk={handleOk} onOk={handleOk}
onCancel={() => toggleVisible(false)} onCancel={() => toggleVisible(false)}
maskClosable={false}
style={{ maxWidth: '96vw' }} style={{ maxWidth: '96vw' }}
footer={ footer={
<> <>

View File

@ -21,51 +21,23 @@
margin: 0 -24px; margin: 0 -24px;
flex-wrap: nowrap; flex-wrap: nowrap;
> aside { :global {
width: 240px; .semi-navigation-inner {
height: 100%; flex-direction: column;
padding: 12px 0;
flex-shrink: 0;
border-right: 1px solid var(--semi-color-border);
overflow: auto;
> ul { .semi-navigation-header-list-outer {
padding: 0; flex: 1;
margin: 0; height: calc(100% - 64px);
list-style: none; }
> li { .semi-navigation-footer {
width: 100%; height: 64px;
padding: 12px 16px;
font-size: 14px;
color: var(--semi-color-text-0);
text-align: center;
cursor: pointer;
border-radius: var(--semi-border-radius-small);
&:hover {
background-color: var(--semi-color-primary-light-default);
}
&.selected {
color: var(--semi-color-primary);
background-color: var(--semi-color-primary-light-default);
}
} }
} }
} }
> main { .selected {
padding: 24px 0; color: var(--semi-color-primary);
overflow: auto; background-color: var(--semi-color-primary-light-default);
background-color: var(--semi-color-nav-bg);
flex: 1;
.editorWrap {
min-height: 100%;
padding: 12px 24px;
background-color: var(--semi-color-bg-2);
border: 1px solid var(--semi-color-border);
}
} }
} }

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { Button, Modal, Typography } from '@douyinfe/semi-ui'; import { Button, Modal, Typography, Layout, Nav } from '@douyinfe/semi-ui';
import { IconChevronLeft } from '@douyinfe/semi-icons'; import { IconChevronLeft } from '@douyinfe/semi-icons';
import { useEditor, EditorContent } from '@tiptap/react'; import { useEditor, EditorContent } from '@tiptap/react';
import cls from 'classnames'; import cls from 'classnames';
@ -16,6 +16,7 @@ interface IProps {
onSelect?: (data) => void; onSelect?: (data) => void;
} }
const { Sider, Content } = Layout;
const { Title } = Typography; const { Title } = Typography;
export const DocumentVersion: React.FC<IProps> = ({ documentId, onSelect }) => { export const DocumentVersion: React.FC<IProps> = ({ documentId, onSelect }) => {
@ -105,28 +106,40 @@ export const DocumentVersion: React.FC<IProps> = ({ documentId, onSelect }) => {
error={error} error={error}
empty={!loading && !data.length} empty={!loading && !data.length}
normalContent={() => ( normalContent={() => (
<div className={styles.contentWrap}> <Layout className={styles.contentWrap}>
<aside> <Sider style={{ backgroundColor: 'var(--semi-color-bg-1)' }}>
<ul> <Nav
style={{ maxWidth: 200, height: '100%' }}
bodyStyle={{ height: '100%' }}
selectedKeys={[selectedVersion]}
footer={{
collapseButton: true,
}}
>
{data.map(({ version, data }) => { {data.map(({ version, data }) => {
return ( return (
<li <Nav.Item
key={version} key={version}
itemKey={version}
className={cls(selectedVersion && selectedVersion.version === version && styles.selected)} className={cls(selectedVersion && selectedVersion.version === version && styles.selected)}
text={<LocaleTime date={+version} />}
onClick={() => select({ version, data })} onClick={() => select({ version, data })}
> />
<LocaleTime date={+version} />
</li>
); );
})} })}
</ul> </Nav>
</aside> </Sider>
<main> <Content
<div className={cls('container', styles.editorWrap)}> style={{
padding: 16,
backgroundColor: 'var(--semi-color-bg-0)',
}}
>
<div className={'container'}>
<EditorContent editor={editor} /> <EditorContent editor={editor} />
</div> </div>
</main> </Content>
</div> </Layout>
)} )}
/> />
</Modal> </Modal>

View File

@ -1,9 +1,10 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Popover, Typography } from '@douyinfe/semi-ui'; import { Popover, Typography, Modal } from '@douyinfe/semi-ui';
import { EXPRESSIONES, GESTURES, SYMBOLS, OBJECTS, ACTIVITIES, SKY_WEATHER } from './constants'; import { EXPRESSIONES, GESTURES, SYMBOLS, OBJECTS, ACTIVITIES, SKY_WEATHER } from './constants';
import { createKeysLocalStorageLRUCache } from 'helpers/lru-cache'; import { createKeysLocalStorageLRUCache } from 'helpers/lru-cache';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import styles from './index.module.scss'; import styles from './index.module.scss';
import { useWindowSize } from 'hooks/use-window-size';
const { Title } = Typography; const { Title } = Typography;
@ -41,6 +42,7 @@ interface IProps {
} }
export const EmojiPicker: React.FC<IProps> = ({ onSelectEmoji, children }) => { export const EmojiPicker: React.FC<IProps> = ({ onSelectEmoji, children }) => {
const { isMobile } = useWindowSize();
const [recentUsed, setRecentUsed] = useState([]); const [recentUsed, setRecentUsed] = useState([]);
const [visible, toggleVisible] = useToggle(false); const [visible, toggleVisible] = useToggle(false);
const renderedList = useMemo( const renderedList = useMemo(
@ -57,6 +59,30 @@ export const EmojiPicker: React.FC<IProps> = ({ onSelectEmoji, children }) => {
[onSelectEmoji] [onSelectEmoji]
); );
const content = useMemo(
() => (
<div className={styles.wrap} style={{ paddingBottom: isMobile ? 24 : 0 }}>
{renderedList.map((item, index) => {
return (
<div key={item.title} className={styles.sectionWrap}>
<Title heading={6} style={{ margin: `${index === 0 ? 0 : 16}px 0 6px` }}>
{item.title}
</Title>
<ul className={styles.listWrap}>
{(item.data || []).map((ex) => (
<li key={ex} onClick={() => selectEmoji(ex)}>
{ex}
</li>
))}
</ul>
</div>
);
})}
</div>
),
[isMobile, renderedList, selectEmoji]
);
useEffect(() => { useEffect(() => {
if (!visible) return; if (!visible) return;
emojiLocalStorageLRUCache.syncFromStorage(); emojiLocalStorageLRUCache.syncFromStorage();
@ -64,35 +90,34 @@ export const EmojiPicker: React.FC<IProps> = ({ onSelectEmoji, children }) => {
}, [visible]); }, [visible]);
return ( return (
<Popover <span>
showArrow {isMobile ? (
zIndex={10000} <>
trigger="click" <Modal
position="bottomLeft" centered
visible={visible} title="表情"
onVisibleChange={toggleVisible} visible={visible}
content={ footer={null}
<div className={styles.wrap}> onCancel={() => toggleVisible(false)}
{renderedList.map((item, index) => { style={{ maxWidth: '96vw' }}
return ( >
<div key={item.title} className={styles.sectionWrap}> {content}
<Title heading={6} style={{ margin: `${index === 0 ? 0 : 16}px 0 6px` }}> </Modal>
{item.title} <span onMouseDown={() => toggleVisible(true)}>{children}</span>
</Title> </>
<ul className={styles.listWrap}> ) : (
{(item.data || []).map((ex) => ( <Popover
<li key={ex} onClick={() => selectEmoji(ex)}> showArrow
{ex} zIndex={10000}
</li> trigger="click"
))} position="bottomLeft"
</ul> visible={visible}
</div> onVisibleChange={toggleVisible}
); content={content}
})} >
</div> {children}
} </Popover>
> )}
{children} </span>
</Popover>
); );
}; };

View File

@ -1,6 +1,6 @@
import React, { useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { Typography, Dropdown, Badge, Button, Tabs, TabPane, Pagination, Notification } from '@douyinfe/semi-ui'; import { Typography, Dropdown, Badge, Button, Tabs, TabPane, Pagination, Notification, Modal } from '@douyinfe/semi-ui';
import { IconMessage } from 'components/icons/IconMessage'; import { IconMessage } from 'components/icons/IconMessage';
import { useAllMessages, useReadMessages, useUnreadMessages } from 'data/message'; import { useAllMessages, useReadMessages, useUnreadMessages } from 'data/message';
import { EmptyBoxIllustration } from 'illustrations/empty-box'; import { EmptyBoxIllustration } from 'illustrations/empty-box';
@ -9,6 +9,8 @@ import { Empty } from 'components/empty';
import { Placeholder } from './placeholder'; import { Placeholder } from './placeholder';
import styles from './index.module.scss'; import styles from './index.module.scss';
import { useUser } from 'data/user'; import { useUser } from 'data/user';
import { useWindowSize } from 'hooks/use-window-size';
import { useToggle } from 'hooks/use-toggle';
const { Text } = Typography; const { Text } = Typography;
const PAGE_SIZE = 6; const PAGE_SIZE = 6;
@ -84,6 +86,8 @@ const MessagesRender = ({ messageData, loading, error, onClick = null, page = 1,
}; };
const MessageBox = () => { const MessageBox = () => {
const { isMobile } = useWindowSize();
const [visible, toggleVisible] = useToggle(false);
const { data: allMsgs, loading: allLoading, error: allError, page: allPage, setPage: allSetPage } = useAllMessages(); const { data: allMsgs, loading: allLoading, error: allError, page: allPage, setPage: allSetPage } = useAllMessages();
const { const {
data: readMsgs, data: readMsgs,
@ -109,6 +113,11 @@ const MessageBox = () => {
); );
}; };
const openModalOnMobile = useCallback(() => {
if (!isMobile) return;
toggleVisible(true);
}, [isMobile, toggleVisible]);
useEffect(() => { useEffect(() => {
if (!unreadMsgs || !unreadMsgs.total) return; if (!unreadMsgs || !unreadMsgs.total) return;
@ -149,69 +158,92 @@ const MessageBox = () => {
}); });
}, [unreadMsgs, readMessage]); }, [unreadMsgs, readMessage]);
return ( const content = (
<Dropdown <Tabs
position="bottomRight" type="line"
trigger="click" size="small"
content={ tabBarExtraContent={
<div style={{ width: 300, padding: '16px 16px 0' }}> unreadMsgs && unreadMsgs.total > 0 ? (
<Tabs <Text type="quaternary" onClick={clearAll} style={{ cursor: 'pointer' }}>
type="line"
size="small" </Text>
tabBarExtraContent={ ) : null
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>
</div>
} }
> >
<Button <TabPane tab="未读" itemKey="unread">
type="tertiary" <MessagesRender
theme="borderless" messageData={unreadMsgs}
icon={ loading={unreadLoading}
unreadMsgs && unreadMsgs.total > 0 ? ( error={unreadError}
<Badge count={unreadMsgs.total} overflowCount={99} type="danger"> onClick={readMessage}
<IconMessage style={{ transform: `translateY(2px)` }} /> page={unreadPage}
</Badge> onPageChange={unreadSetPage}
) : ( />
<IconMessage /> </TabPane>
) <TabPane tab="已读" itemKey="read">
} <MessagesRender
></Button> messageData={readMsgs}
</Dropdown> 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 btn = (
<Button
type="tertiary"
theme="borderless"
icon={
unreadMsgs && unreadMsgs.total > 0 ? (
<Badge count={unreadMsgs.total} overflowCount={99} type="danger">
<IconMessage style={{ transform: `translateY(2px)` }} />
</Badge>
) : (
<IconMessage />
)
}
onClick={openModalOnMobile}
/>
);
return (
<span>
{isMobile ? (
<>
<Modal
centered
title="最近访问"
visible={visible}
footer={null}
onCancel={toggleVisible}
style={{ maxWidth: '96vw' }}
>
{content}
</Modal>
{btn}
</>
) : (
<Dropdown
position="bottomRight"
trigger="click"
content={<div style={{ width: 300, padding: '16px 16px 0' }}>{content}</div>}
>
{btn}
</Dropdown>
)}
</span>
); );
}; };

View File

@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import { Button, Tooltip } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { IconSun, IconMoon } from '@douyinfe/semi-icons'; import { IconSun, IconMoon } from '@douyinfe/semi-icons';
import { useTheme } from 'hooks/use-theme'; import { useTheme } from 'hooks/use-theme';
import { Tooltip } from 'components/tooltip';
export const Theme = () => { export const Theme = () => {
const { theme, toggle } = useTheme(); const { theme, toggle } = useTheme();

View File

@ -21,7 +21,7 @@ export const Tooltip: React.FC<IProps> = ({ content, hideOnClick = false, positi
onMouseLeave={() => { onMouseLeave={() => {
toggleVisible(false); toggleVisible(false);
}} }}
onClick={() => { onMouseMove={() => {
hideOnClick && toggleVisible(false); hideOnClick && toggleVisible(false);
}} }}
> >

View File

@ -25,7 +25,7 @@ export const User: React.FC = () => {
<> <>
<Dropdown <Dropdown
trigger="click" trigger="click"
position="bottomLeft" position="bottomRight"
render={ render={
<Dropdown.Menu style={{ width: 160 }}> <Dropdown.Menu style={{ width: 160 }}>
<Dropdown.Item onClick={() => toggleVisible(true)}> <Dropdown.Item onClick={() => toggleVisible(true)}>

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { Dropdown, Button } from '@douyinfe/semi-ui'; import { Dropdown, Button } from '@douyinfe/semi-ui';
import { IconChevronDown } from '@douyinfe/semi-icons'; import { IconChevronDown, IconPlus } from '@douyinfe/semi-icons';
import { useWindowSize } from 'hooks/use-window-size';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import { useQuery } from 'hooks/use-query'; import { useQuery } from 'hooks/use-query';
import { WikiCreator } from 'components/wiki/create'; import { WikiCreator } from 'components/wiki/create';
@ -11,6 +12,7 @@ interface IProps {
} }
export const WikiOrDocumentCreator: React.FC<IProps> = ({ onCreateDocument, children }) => { export const WikiOrDocumentCreator: React.FC<IProps> = ({ onCreateDocument, children }) => {
const { isMobile } = useWindowSize();
const { wikiId, docId } = useQuery<{ wikiId?: string; docId?: string }>(); const { wikiId, docId } = useQuery<{ wikiId?: string; docId?: string }>();
const [visible, toggleVisible] = useToggle(false); const [visible, toggleVisible] = useToggle(false);
const [createDocumentModalVisible, toggleCreateDocumentModalVisible] = useToggle(false); const [createDocumentModalVisible, toggleCreateDocumentModalVisible] = useToggle(false);
@ -25,7 +27,9 @@ export const WikiOrDocumentCreator: React.FC<IProps> = ({ onCreateDocument, chil
</Dropdown.Menu> </Dropdown.Menu>
} }
> >
{children || ( {children || isMobile ? (
<Button type="primary" theme="solid" icon={<IconPlus />} size="small" />
) : (
<Button type="primary" theme="solid" icon={<IconChevronDown />} iconPosition="right"> <Button type="primary" theme="solid" icon={<IconChevronDown />} iconPosition="right">
</Button> </Button>

View File

@ -13,6 +13,7 @@
.treeInnerWrap { .treeInnerWrap {
:global { :global {
.semi-tree-option-list-block .semi-tree-option-selected { .semi-tree-option-list-block .semi-tree-option-selected {
font-weight: 600;
color: var(--semi-color-primary); color: var(--semi-color-primary);
background-color: var(--semi-color-primary-light-default); background-color: var(--semi-color-primary-light-default);
} }
@ -55,6 +56,7 @@
} }
&.isActive { &.isActive {
font-weight: 600;
color: var(--semi-color-primary); color: var(--semi-color-primary);
background-color: var(--semi-color-primary-light-default); background-color: var(--semi-color-primary-light-default);
} }
@ -90,7 +92,7 @@
.title { .title {
overflow: hidden; overflow: hidden;
color: var(--semi-color-text-0); color: inherit;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
flex: 1; flex: 1;

View File

@ -77,6 +77,7 @@ export const Tree = ({ data, docAsLink, getDocLink, parentIds, activeId, isShare
ellipsis={{ ellipsis={{
showTooltip: { opts: { content: label, style: { wordBreak: 'break-all' }, position: 'right' } }, showTooltip: { opts: { content: label, style: { wordBreak: 'break-all' }, position: 'right' } },
}} }}
style={{ color: 'inherit' }}
> >
{label} {label}
</Typography.Text> </Typography.Text>

View File

@ -66,7 +66,7 @@ export const useDragableWidth = () => {
setStorage(key, nextWidth); setStorage(key, nextWidth);
} }
mutate(); mutate();
}, [mutate, currentWidth, minWidth]); }, [mutate, currentWidth, minWidth, maxWidth]);
useEffect(() => { useEffect(() => {
const min = windowWidth <= PC_MOBILE_CRITICAL_WIDTH ? DEFAULT_MOBILE_MIN_WIDTH : DEFAULT_PC_MIN_WIDTH; const min = windowWidth <= PC_MOBILE_CRITICAL_WIDTH ? DEFAULT_MOBILE_MIN_WIDTH : DEFAULT_PC_MIN_WIDTH;

View File

@ -3,12 +3,16 @@ import { useState, useEffect } from 'react';
interface Size { interface Size {
width: number | undefined; width: number | undefined;
height: number | undefined; height: number | undefined;
isMobile: boolean;
} }
const PC_MOBILE_CRITICAL_WIDTH = 765;
export function useWindowSize(): Size { export function useWindowSize(): Size {
const [windowSize, setWindowSize] = useState<Size>({ const [windowSize, setWindowSize] = useState<Size>({
width: undefined, width: undefined,
height: undefined, height: undefined,
isMobile: false,
}); });
useEffect(() => { useEffect(() => {
@ -16,6 +20,7 @@ export function useWindowSize(): Size {
setWindowSize({ setWindowSize({
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight, height: window.innerHeight,
isMobile: window.innerWidth <= PC_MOBILE_CRITICAL_WIDTH,
}); });
} }
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);

View File

@ -40,3 +40,16 @@
} }
} }
} }
.mobileHeader {
display: flex;
width: 100%;
height: 60px;
padding-right: 24px;
padding-left: 24px;
border-right: none;
border-bottom: 1px solid var(--semi-color-border);
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
}

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Layout as SemiLayout, Nav, Space } from '@douyinfe/semi-ui'; import { Layout as SemiLayout, Nav, Space, Typography, Dropdown, Button } from '@douyinfe/semi-ui';
import { IconMenu } from '@douyinfe/semi-icons';
import Router, { useRouter } from 'next/router'; import Router, { useRouter } from 'next/router';
import Link from 'next/link';
import { User } from 'components/user'; import { User } from 'components/user';
import { WikiOrDocumentCreator } from 'components/wiki-or-document-creator'; import { WikiOrDocumentCreator } from 'components/wiki-or-document-creator';
import { LogoImage, LogoText } from 'components/logo'; import { LogoImage, LogoText } from 'components/logo';
@ -9,19 +9,18 @@ import { Theme } from 'components/theme';
import { Message } from 'components/message'; import { Message } from 'components/message';
import { Search } from 'components/search'; import { Search } from 'components/search';
import { useWindowSize } from 'hooks/use-window-size'; import { useWindowSize } from 'hooks/use-window-size';
import { Recent } from './recent'; import { useToggle } from 'hooks/use-toggle';
import { Wiki } from './wiki'; import { Recent, RecentModal } from './recent';
import { Wiki, WikiModal } from './wiki';
import styles from './index.module.scss';
const { Header: SemiHeader } = SemiLayout; const { Header: SemiHeader } = SemiLayout;
const { Text } = Typography;
const menus = [ const menus = [
{ {
itemKey: '/', itemKey: '/',
text: ( text: '主页',
<Link href="/">
<a></a>
</Link>
),
onClick: () => { onClick: () => {
Router.push({ Router.push({
pathname: `/`, pathname: `/`,
@ -38,11 +37,7 @@ const menus = [
}, },
{ {
itemKey: '/star', itemKey: '/star',
text: ( text: '收藏',
<Link href="/star">
<a></a>
</Link>
),
onClick: () => { onClick: () => {
Router.push({ Router.push({
pathname: `/star`, pathname: `/star`,
@ -51,11 +46,7 @@ const menus = [
}, },
{ {
itemKey: '/template', itemKey: '/template',
text: ( text: '模板',
<Link href="/template">
<a></a>
</Link>
),
onClick: () => { onClick: () => {
Router.push({ Router.push({
pathname: `/template`, pathname: `/template`,
@ -64,11 +55,7 @@ const menus = [
}, },
{ {
itemKey: '/find', itemKey: '/find',
text: ( text: '发现',
<Link href="/find">
<a></a>
</Link>
),
onClick: () => { onClick: () => {
Router.push({ Router.push({
pathname: `/find`, pathname: `/find`,
@ -79,22 +66,47 @@ const menus = [
export const RouterHeader: React.FC = () => { export const RouterHeader: React.FC = () => {
const { pathname } = useRouter(); const { pathname } = useRouter();
const windowSize = useWindowSize(); const { width, isMobile } = useWindowSize();
const [recentModalVisible, toggleRecentModalVisible] = useToggle(false);
const [wikiModalVisible, toggleWikiModalVisible] = useToggle(false);
return ( return (
<SemiHeader> <SemiHeader>
<Nav {isMobile ? (
mode="horizontal" <div className={styles.mobileHeader}>
style={{ overflow: 'auto' }}
header={
<Space> <Space>
<LogoImage /> <LogoImage />
{windowSize.width >= 890 && <LogoText />} <LogoText />
<RecentModal visible={recentModalVisible} toggleVisible={toggleRecentModalVisible} />
<WikiModal visible={wikiModalVisible} toggleVisible={toggleWikiModalVisible} />
<Dropdown
trigger="click"
position="bottomRight"
render={
<Dropdown.Menu>
{menus.slice(0, 1).map((menu) => {
return (
<Dropdown.Item key={menu.itemKey} onClick={menu.onClick}>
{menu.text}
</Dropdown.Item>
);
})}
<Dropdown.Item onClick={toggleRecentModalVisible}></Dropdown.Item>
<Dropdown.Item onClick={toggleWikiModalVisible}></Dropdown.Item>
{menus.slice(3).map((menu) => {
return (
<Dropdown.Item key={menu.itemKey} onClick={menu.onClick}>
{menu.text}
</Dropdown.Item>
);
})}
</Dropdown.Menu>
}
>
<Button icon={<IconMenu />} type="tertiary" theme="borderless" />
</Dropdown>
</Space> </Space>
}
selectedKeys={[pathname || '/']}
items={menus}
footer={
<Space> <Space>
<WikiOrDocumentCreator /> <WikiOrDocumentCreator />
<Search /> <Search />
@ -102,8 +114,30 @@ export const RouterHeader: React.FC = () => {
<Theme /> <Theme />
<User /> <User />
</Space> </Space>
} </div>
></Nav> ) : (
<Nav
mode="horizontal"
style={{ overflow: 'auto' }}
header={
<Space>
<LogoImage />
{width >= 890 && <LogoText />}
</Space>
}
selectedKeys={[pathname || '/']}
items={menus}
footer={
<Space>
<WikiOrDocumentCreator />
<Search />
<Message />
<Theme />
<User />
</Space>
}
></Nav>
)}
</SemiHeader> </SemiHeader>
); );
}; };

View File

@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { Typography, Space, Dropdown, Tabs, TabPane } from '@douyinfe/semi-ui'; import { Typography, Space, Dropdown, Tabs, TabPane, Modal } from '@douyinfe/semi-ui';
import { IconChevronDown } from '@douyinfe/semi-icons'; import { IconChevronDown } from '@douyinfe/semi-icons';
import { useRecentDocuments } from 'data/document'; import { useRecentDocuments } from 'data/document';
import { useToggle } from 'hooks/use-toggle';
import { Empty } from 'components/empty'; import { Empty } from 'components/empty';
import { DataRender } from 'components/data-render'; import { DataRender } from 'components/data-render';
import { LocaleTime } from 'components/locale-time'; import { LocaleTime } from 'components/locale-time';
@ -13,78 +14,104 @@ import styles from './index.module.scss';
const { Text } = Typography; const { Text } = Typography;
export const Recent = () => { export const RecentDocs = () => {
const { data: recentDocs, loading, error } = useRecentDocuments(); const { data: recentDocs, loading, error } = useRecentDocuments();
return ( return (
<Dropdown <Tabs type="line" size="small">
trigger="click" <TabPane tab="文档" itemKey="docs">
spacing={16} <DataRender
content={ loading={loading}
<div style={{ width: 300, padding: '16px 16px 0' }}> loadingContent={<Placeholder />}
<Tabs type="line" size="small"> error={error}
<TabPane tab="文档" itemKey="docs"> normalContent={() => {
<DataRender return (
loading={loading} <div className={styles.itemsWrap} style={{ margin: '0 -16px' }}>
loadingContent={<Placeholder />} {recentDocs.length ? (
error={error} recentDocs.map((doc) => {
normalContent={() => { return (
return ( <div className={styles.itemWrap} key={doc.id}>
<div className={styles.itemsWrap} style={{ margin: '0 -16px' }}> <Link
{recentDocs.length ? ( href={{
recentDocs.map((doc) => { pathname: '/wiki/[wikiId]/document/[documentId]',
return ( query: {
<div className={styles.itemWrap} key={doc.id}> wikiId: doc.wikiId,
<Link documentId: doc.id,
href={{ },
pathname: '/wiki/[wikiId]/document/[documentId]', }}
query: { >
wikiId: doc.wikiId, <a className={styles.item}>
documentId: doc.id, <div className={styles.leftWrap}>
}, <IconDocumentFill style={{ marginRight: 12 }} />
}} <div>
> <Text ellipsis={{ showTooltip: true }} style={{ width: 180 }}>
<a className={styles.item}> {doc.title}
<div className={styles.leftWrap}> </Text>
<IconDocumentFill style={{ marginRight: 12 }} />
<div>
<Text ellipsis={{ showTooltip: true }} style={{ width: 180 }}>
{doc.title}
</Text>
<Text size="small" type="tertiary"> <Text size="small" type="tertiary">
{doc.createUser && doc.createUser.name} {' '} {doc.createUser && doc.createUser.name} <LocaleTime date={doc.updatedAt} timeago />
<LocaleTime date={doc.updatedAt} timeago /> </Text>
</Text> </div>
</div>
</div>
<div className={styles.rightWrap}>
<DocumentStar documentId={doc.id} />
</div>
</a>
</Link>
</div> </div>
); <div className={styles.rightWrap}>
}) <DocumentStar documentId={doc.id} />
) : ( </div>
<Empty message="最近访问的文档会出现在此处" /> </a>
)} </Link>
</div> </div>
); );
}} })
/> ) : (
</TabPane> <Empty message="最近访问的文档会出现在此处" />
</Tabs> )}
</div> </div>
} );
> }}
<span> />
<Space> </TabPane>
</Tabs>
<IconChevronDown /> );
</Space> };
</span>
</Dropdown> export const RecentModal = ({ visible, toggleVisible }) => {
return (
<Modal
centered
title="最近访问"
visible={visible}
footer={null}
onCancel={toggleVisible}
style={{ maxWidth: '96vw' }}
>
<RecentDocs />
</Modal>
);
};
export const RecentMobileTrigger = ({ toggleVisible }) => {
return <span onClick={toggleVisible}></span>;
};
export const Recent = () => {
return (
<span>
<Dropdown
trigger="click"
spacing={16}
content={
<div style={{ width: 300, padding: '16px 16px 0' }}>
<RecentDocs />
</div>
}
>
<span>
<Space>
<IconChevronDown />
</Space>
</span>
</Dropdown>
</span>
); );
}; };

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import Link from 'next/link'; import Link from 'next/link';
import { Avatar, Typography, Space, Dropdown } from '@douyinfe/semi-ui'; import { Avatar, Typography, Space, Dropdown, Modal } from '@douyinfe/semi-ui';
import { IconChevronDown } from '@douyinfe/semi-icons'; import { IconChevronDown } from '@douyinfe/semi-icons';
import { useStaredWikis, useWikiDetail } from 'data/wiki'; import { useStaredWikis, useWikiDetail } from 'data/wiki';
import { Empty } from 'components/empty'; import { Empty } from 'components/empty';
@ -12,11 +12,150 @@ import styles from './index.module.scss';
const { Text } = Typography; const { Text } = Typography;
export const Wiki = () => { const WikiContent = () => {
const { query } = useRouter(); const { query } = useRouter();
const { data: starWikis, loading, error, refresh: refreshStarWikis } = useStaredWikis(); const { data: starWikis, loading, error, refresh: refreshStarWikis } = useStaredWikis();
const { data: currentWiki } = useWikiDetail(query.wikiId); const { data: currentWiki } = useWikiDetail(query.wikiId);
return (
<>
{currentWiki && (
<>
<div className={styles.titleWrap}>
<Text strong type="secondary">
</Text>
</div>
<div className={styles.itemWrap}>
<Link
href={{
pathname: '/wiki/[wikiId]',
query: {
wikiId: currentWiki.id,
},
}}
>
<a className={styles.item}>
<div className={styles.leftWrap}>
<Avatar
shape="square"
size="small"
src={currentWiki.avatar}
style={{
marginRight: 8,
width: 24,
height: 24,
borderRadius: 4,
}}
>
{currentWiki.name.charAt(0)}
</Avatar>
<div>
<Text ellipsis={{ showTooltip: true }} style={{ width: 180 }}>
{currentWiki.name}
</Text>
</div>
</div>
<div className={styles.rightWrap}>
<WikiStar wikiId={currentWiki.id} onChange={refreshStarWikis} />
</div>
</a>
</Link>
</div>
</>
)}
<div className={styles.titleWrap}>
<Text strong type="secondary">
</Text>
</div>
<DataRender
loading={loading}
loadingContent={<Placeholder />}
error={error}
normalContent={() => {
return (
<div className={styles.itemsWrap}>
{starWikis.length ? (
starWikis.map((wiki) => {
return (
<div className={styles.itemWrap} key={wiki.id}>
<Link
href={{
pathname: '/wiki/[wikiId]',
query: {
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 wikiId={wiki.id} onChange={refreshStarWikis} />
</div>
</a>
</Link>
</div>
);
})
) : (
<Empty message="收藏的知识库会出现在此处" />
)}
</div>
);
}}
/>
<Dropdown.Divider />
<div className={styles.itemWrap}>
<Link
href={{
pathname: '/wiki',
}}
>
<a className={styles.item} style={{ padding: '12px 16px' }}>
<Text></Text>
</a>
</Link>
</div>
</>
);
};
export const WikiModal = ({ visible, toggleVisible }) => {
return (
<Modal
centered
title="最近访问"
visible={visible}
footer={null}
onCancel={toggleVisible}
style={{ maxWidth: '96vw' }}
>
<WikiContent />
</Modal>
);
};
export const Wiki = () => {
return ( return (
<Dropdown <Dropdown
trigger="click" trigger="click"
@ -28,123 +167,7 @@ export const Wiki = () => {
paddingBottom: 8, paddingBottom: 8,
}} }}
> >
{currentWiki && ( <WikiContent />
<>
<div className={styles.titleWrap}>
<Text strong type="secondary">
</Text>
</div>
<div className={styles.itemWrap}>
<Link
href={{
pathname: '/wiki/[wikiId]',
query: {
wikiId: currentWiki.id,
},
}}
>
<a className={styles.item}>
<div className={styles.leftWrap}>
<Avatar
shape="square"
size="small"
src={currentWiki.avatar}
style={{
marginRight: 8,
width: 24,
height: 24,
borderRadius: 4,
}}
>
{currentWiki.name.charAt(0)}
</Avatar>
<div>
<Text ellipsis={{ showTooltip: true }} style={{ width: 180 }}>
{currentWiki.name}
</Text>
</div>
</div>
<div className={styles.rightWrap}>
<WikiStar wikiId={currentWiki.id} onChange={refreshStarWikis} />
</div>
</a>
</Link>
</div>
</>
)}
<div className={styles.titleWrap}>
<Text strong type="secondary">
</Text>
</div>
<DataRender
loading={loading}
loadingContent={<Placeholder />}
error={error}
normalContent={() => {
return (
<div className={styles.itemsWrap}>
{starWikis.length ? (
starWikis.map((wiki) => {
return (
<div className={styles.itemWrap} key={wiki.id}>
<Link
href={{
pathname: '/wiki/[wikiId]',
query: {
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 wikiId={wiki.id} onChange={refreshStarWikis} />
</div>
</a>
</Link>
</div>
);
})
) : (
<Empty message="收藏的知识库会出现在此处" />
)}
</div>
);
}}
/>
<Dropdown.Divider />
<div className={styles.itemWrap}>
<Link
href={{
pathname: '/wiki',
}}
>
<a className={styles.item} style={{ padding: '12px 16px' }}>
<Text></Text>
</a>
</Link>
</div>
</div> </div>
} }
> >

View File

@ -1,6 +1,8 @@
import React from 'react'; import React, { useMemo } from 'react';
import { Dropdown, Typography } from '@douyinfe/semi-ui'; import { Dropdown, Typography, Modal } from '@douyinfe/semi-ui';
import styles from './style.module.scss'; import styles from './style.module.scss';
import { useWindowSize } from 'hooks/use-window-size';
import { useToggle } from 'hooks/use-toggle';
const { Text } = Typography; const { Text } = Typography;
@ -78,36 +80,60 @@ const colors = [
]; ];
export const ColorPicker: React.FC<{ export const ColorPicker: React.FC<{
onSetColor; title?: string;
onSetColor: (arg: string) => void;
disabled?: boolean; disabled?: boolean;
}> = ({ children, onSetColor, disabled = false }) => { }> = ({ children, title = '颜色管理', onSetColor, disabled = false }) => {
const { isMobile } = useWindowSize();
const [visible, toggleVisible] = useToggle(false);
const content = useMemo(
() => (
<div style={{ padding: isMobile ? '0 0 24px' : '12px 16px' }}>
<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>
</div>
),
[onSetColor, isMobile]
);
if (disabled) return <span style={{ display: 'inline-block' }}>{children}</span>; if (disabled) return <span style={{ display: 'inline-block' }}>{children}</span>;
return ( return (
<Dropdown <span>
zIndex={10000} {isMobile ? (
trigger="click" <>
position={'bottomLeft'} <Modal
render={ centered
<div style={{ padding: '8px 0' }}> title={title}
<div className={styles.emptyWrap} onClick={() => onSetColor(null)}> visible={visible}
<span></span> footer={null}
<Text></Text> onCancel={() => toggleVisible(false)}
</div> style={{ maxWidth: '96vw', width: 288 }}
>
<div className={styles.colorWrap}> {content}
{colors.map((color) => { </Modal>
return ( <span style={{ display: 'inline-block' }} onMouseDown={() => toggleVisible(true)}>
<div key={color} className={styles.colorItem} onClick={() => onSetColor(color)}> {children}
<span style={{ backgroundColor: color }}></span> </span>
</div> </>
); ) : (
})} <Dropdown zIndex={10000} trigger="click" position={'bottomLeft'} render={content}>
</div> <span style={{ display: 'inline-block' }}>{children}</span>
</div> </Dropdown>
} )}
> </span>
<span style={{ display: 'inline-block' }}>{children}</span>
</Dropdown>
); );
}; };

View File

@ -1,8 +1,9 @@
.emptyWrap { .emptyWrap {
display: flex; display: flex;
flex-wrap: nowrap; width: 240px;
padding: 8px 10px;
cursor: pointer; cursor: pointer;
border: 1px solid transparent;
flex-wrap: nowrap;
&:hover { &:hover {
background-color: var(--semi-color-fill-1); background-color: var(--semi-color-fill-1);
@ -11,9 +12,9 @@
> span:first-child { > span:first-child {
position: relative; position: relative;
display: block; display: block;
width: 18px; width: 20px;
height: 18px; height: 20px;
margin-right: 8px; margin: 0 8px 0 1px;
border: 1px solid #e8e8e8; border: 1px solid #e8e8e8;
border-radius: 2px; border-radius: 2px;
@ -34,8 +35,8 @@
.colorWrap { .colorWrap {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
width: 256px; width: 240px;
padding: 8px; margin-top: 8px;
.colorItem { .colorItem {
display: flex; display: flex;

View File

@ -1,18 +1,21 @@
import React, { useEffect, forwardRef, useImperativeHandle, useRef, useMemo } from 'react'; import React, { useEffect, forwardRef, useImperativeHandle, useRef, useMemo } from 'react';
import { Toast, BackTop } from '@douyinfe/semi-ui'; import { Toast, BackTop } from '@douyinfe/semi-ui';
import { HocuspocusProvider } from '@hocuspocus/provider'; import { HocuspocusProvider } from '@hocuspocus/provider';
import cls from 'classnames';
import { debounce } from 'helpers/debounce'; import { debounce } from 'helpers/debounce';
import { useNetwork } from 'hooks/use-network'; import { useNetwork } from 'hooks/use-network';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import { useWindowSize } from 'hooks/use-window-size';
import { LogoName } from 'components/logo'; import { LogoName } from 'components/logo';
import { Banner } from 'components/banner'; import { Banner } from 'components/banner';
import { useEditor, EditorContent } from '../../react';
import { Collaboration } from 'tiptap/core/extensions/collaboration'; import { Collaboration } from 'tiptap/core/extensions/collaboration';
import { CollaborationCursor } from 'tiptap/core/extensions/collaboration-cursor'; import { CollaborationCursor } from 'tiptap/core/extensions/collaboration-cursor';
import { getRandomColor } from 'helpers/color'; import { getRandomColor } from 'helpers/color';
import { useEditor, EditorContent } from '../../react';
import { CollaborationKit } from '../kit'; import { CollaborationKit } from '../kit';
import { MenuBar } from './menubar'; import { MenuBar } from './menubar';
import { ICollaborationEditorProps, ProviderStatus } from './type'; import { ICollaborationEditorProps, ProviderStatus } from './type';
import styles from './index.module.scss';
type IProps = Pick< type IProps = Pick<
ICollaborationEditorProps, ICollaborationEditorProps,
@ -25,6 +28,7 @@ type IProps = Pick<
export const EditorInstance = forwardRef((props: IProps, ref) => { export const EditorInstance = forwardRef((props: IProps, ref) => {
const { hocuspocusProvider, editable, user, onTitleUpdate, status, menubar, renderInEditorPortal } = props; const { hocuspocusProvider, editable, user, onTitleUpdate, status, menubar, renderInEditorPortal } = props;
const $mainContainer = useRef<HTMLDivElement>(); const $mainContainer = useRef<HTMLDivElement>();
const { isMobile } = useWindowSize();
const { online } = useNetwork(); const { online } = useNetwork();
const [created, toggleCreated] = useToggle(false); const [created, toggleCreated] = useToggle(false);
const editor = useEditor( const editor = useEditor(
@ -98,7 +102,7 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
<Banner type="warning" description="您没有编辑权限,暂不能编辑该文档。" closeable={false} /> <Banner type="warning" description="您没有编辑权限,暂不能编辑该文档。" closeable={false} />
)} )}
{menubar && ( {menubar && (
<header> <header className={cls(isMobile && styles.mobileToolbar)}>
<MenuBar editor={editor} /> <MenuBar editor={editor} />
</header> </header>
)} )}
@ -107,7 +111,9 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
{protals} {protals}
</main> </main>
{editable && menubar && <BackTop target={() => $mainContainer.current} visibilityHeight={200} />} {editable && menubar && (
<BackTop target={() => $mainContainer.current} style={{ right: 16, bottom: 65 }} visibilityHeight={200} />
)}
</> </>
); );
}); });

View File

@ -17,6 +17,24 @@
align-items: center; align-items: center;
border-bottom: 1px solid var(--semi-color-border); border-bottom: 1px solid var(--semi-color-border);
user-select: none; user-select: none;
&.mobileToolbar {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 999;
display: flex;
height: 49px;
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
background: var(--semi-color-bg-1);
box-sizing: content-box;
justify-content: space-around;
align-items: center;
border-top: 1px solid var(--semi-color-border);
}
} }
> main { > main {

View File

@ -9,7 +9,7 @@ import { Title } from 'tiptap/core/extensions/title';
import { ColorPicker } from 'tiptap/components/color-picker'; import { ColorPicker } from 'tiptap/components/color-picker';
const FlexStyle: React.CSSProperties = { const FlexStyle: React.CSSProperties = {
display: 'flex', display: 'inline-flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
}; };
@ -33,16 +33,16 @@ export const BackgroundColor: React.FC<{ editor: Editor }> = ({ editor }) => {
); );
return ( return (
<ColorPicker onSetColor={setBackgroundColor} disabled={isTitleActive}> <ColorPicker title="背景色" onSetColor={setBackgroundColor} disabled={isTitleActive}>
<Tooltip content="背景色"> <Tooltip content="背景色">
<Button <Button
theme={editor.isActive('textStyle') ? 'light' : 'borderless'} theme={editor.isActive('textStyle') ? 'light' : 'borderless'}
type={'tertiary'} type={'tertiary'}
icon={ icon={
<div style={FlexStyle}> <span style={FlexStyle}>
<IconMark /> <IconMark />
<span style={{ backgroundColor, width: 12, height: 2 }}></span> <span style={{ backgroundColor, width: 12, height: 2 }}></span>
</div> </span>
} }
disabled={isTitleActive} disabled={isTitleActive}
/> />

View File

@ -33,7 +33,14 @@ export const CountdownSettingModal: React.FC<IProps> = ({ editor }) => {
}, [editor, toggleVisible]); }, [editor, toggleVisible]);
return ( return (
<Modal centered title="倒计时" visible={visible} onOk={handleOk} onCancel={() => toggleVisible(false)}> <Modal
centered
title="倒计时"
style={{ maxWidth: '96vw' }}
visible={visible}
onOk={handleOk}
onCancel={() => toggleVisible(false)}
>
<Form initValues={initialState} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left"> <Form initValues={initialState} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left">
<Form.Input labelWidth={72} label="标题" field="title" required /> <Form.Input labelWidth={72} label="标题" field="title" required />
<Form.DatePicker labelWidth={72} style={{ width: '100%' }} label="截止日期" field="date" type="dateTime" /> <Form.DatePicker labelWidth={72} style={{ width: '100%' }} label="截止日期" field="date" type="dateTime" />

View File

@ -71,6 +71,7 @@ export const IframeBubbleMenu = ({ editor }) => {
> >
<Modal <Modal
title="编辑链接" title="编辑链接"
style={{ maxWidth: '96vw' }}
visible={visible} visible={visible}
onOk={handleOk} onOk={handleOk}
onCancel={() => toggleVisible(false)} onCancel={() => toggleVisible(false)}

View File

@ -45,7 +45,14 @@ export const LinkSettingModal: React.FC<IProps> = ({ editor }) => {
}, [editor, toggleVisible]); }, [editor, toggleVisible]);
return ( return (
<Modal title="编辑链接" visible={visible} onOk={handleOk} onCancel={handleCancel} centered> <Modal
title="编辑链接"
style={{ maxWidth: '96vw' }}
visible={visible}
onOk={handleOk}
onCancel={handleCancel}
centered
>
<Form initValues={initialState} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left"> <Form initValues={initialState} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left">
<Form.Input label="文本" field="text" placeholder="请输入文本"></Form.Input> <Form.Input label="文本" field="text" placeholder="请输入文本"></Form.Input>
<Form.Input <Form.Input

View File

@ -1,6 +1,8 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { Popover, Button, Typography, Input, Space } from '@douyinfe/semi-ui'; import { Popover, Button, Typography, Input, Space, Modal } from '@douyinfe/semi-ui';
import { Editor } from 'tiptap/editor'; import { Editor } from 'tiptap/editor';
import { useWindowSize } from 'hooks/use-window-size';
import { useToggle } from 'hooks/use-toggle';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { IconSearchReplace } from 'components/icons'; import { IconSearchReplace } from 'components/icons';
import { SearchNReplace } from 'tiptap/core/extensions/search'; import { SearchNReplace } from 'tiptap/core/extensions/search';
@ -8,17 +10,26 @@ import { SearchNReplace } from 'tiptap/core/extensions/search';
const { Text } = Typography; const { Text } = Typography;
export const Search: React.FC<{ editor: Editor }> = ({ editor }) => { export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
const { isMobile } = useWindowSize();
const [visible, toggleVisible] = useToggle(false);
const [currentIndex, setCurrentIndex] = useState(-1); const [currentIndex, setCurrentIndex] = useState(-1);
const [results, setResults] = useState([]); const [results, setResults] = useState([]);
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [replaceValue, setReplaceValue] = useState(''); const [replaceValue, setReplaceValue] = useState('');
const onVisibleChange = useCallback((visible) => { const openModalOnMobile = useCallback(() => {
if (!isMobile) return;
toggleVisible(true);
}, [isMobile, toggleVisible]);
useEffect(() => {
if (!visible) { if (!visible) {
setSearchValue(''); setSearchValue('');
setReplaceValue(''); setReplaceValue('');
setCurrentIndex(-1);
setResults([]);
} }
}, []); }, [visible]);
useEffect(() => { useEffect(() => {
if (editor && editor.commands && editor.commands.setSearchTerm) { if (editor && editor.commands && editor.commands.setSearchTerm) {
@ -52,55 +63,78 @@ export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
}; };
}, [editor]); }, [editor]);
const content = (
<div style={{ paddingBottom: isMobile ? 24 : 0 }}>
<div style={{ marginBottom: 12 }}>
<Text type="tertiary"></Text>
<Input
autofocus
value={searchValue}
onChange={setSearchValue}
suffix={results.length ? `${currentIndex + 1}/${results.length}` : ''}
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text type="tertiary"></Text>
<Input value={replaceValue} onChange={setReplaceValue} />
</div>
<div>
<Space>
<Button disabled={!results.length} onClick={editor.commands.replaceAll}>
</Button>
<Button disabled={!results.length} onClick={editor.commands.replace}>
</Button>
<Button disabled={!results.length} onClick={editor.commands.goToPrevSearchResult}>
</Button>
<Button disabled={!results.length} onClick={editor.commands.goToNextSearchResult}>
</Button>
</Space>
</div>
</div>
);
const btn = (
<Tooltip content="查找替换">
<Button theme={'borderless'} type="tertiary" icon={<IconSearchReplace />} onMouseDown={openModalOnMobile} />
</Tooltip>
);
return ( return (
<Popover <span>
showArrow {isMobile ? (
zIndex={10000} <>
trigger="click" <Modal
position="bottomRight" centered
onVisibleChange={onVisibleChange} title="查找替换"
content={ visible={visible}
<div> footer={null}
<div style={{ marginBottom: 12 }}> onCancel={() => toggleVisible(false)}
<Text type="tertiary"></Text> style={{ maxWidth: '96vw' }}
<Input >
autofocus {content}
value={searchValue} </Modal>
onChange={setSearchValue} {btn}
suffix={results.length ? `${currentIndex + 1}/${results.length}` : ''} </>
/> ) : (
</div> <Popover
<div style={{ marginBottom: 12 }}> showArrow
<Text type="tertiary"></Text> zIndex={10000}
<Input value={replaceValue} onChange={setReplaceValue} /> trigger="click"
</div> position="bottomRight"
<div> visible={visible}
<Space> onVisibleChange={toggleVisible}
<Button disabled={!results.length} onClick={editor.commands.replaceAll}> content={content}
>
</Button> <span>{btn}</span>
</Popover>
<Button disabled={!results.length} onClick={editor.commands.replace}> )}
</span>
</Button>
<Button disabled={!results.length} onClick={editor.commands.goToPrevSearchResult}>
</Button>
<Button disabled={!results.length} onClick={editor.commands.goToNextSearchResult}>
</Button>
</Space>
</div>
</div>
}
>
<span>
<Tooltip content="查找替换">
<Button theme={'borderless'} type="tertiary" icon={<IconSearchReplace />} />
</Tooltip>
</span>
</Popover>
); );
}; };

View File

@ -12,7 +12,7 @@ import { ColorPicker } from 'tiptap/components/color-picker';
type Color = { color: string }; type Color = { color: string };
const FlexStyle = { const FlexStyle = {
display: 'flex', display: 'inline-flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
} as React.CSSProperties; } as React.CSSProperties;
@ -35,13 +35,13 @@ export const TextColor: React.FC<{ editor: Editor }> = ({ editor }) => {
); );
return ( return (
<ColorPicker onSetColor={setColor} disabled={isTitleActive}> <ColorPicker title="文本色" onSetColor={setColor} disabled={isTitleActive}>
<Tooltip content="文本色"> <Tooltip content="文本色">
<Button <Button
theme={isTextStyleActive ? 'light' : 'borderless'} theme={isTextStyleActive ? 'light' : 'borderless'}
type={'tertiary'} type={'tertiary'}
icon={ icon={
<div style={FlexStyle}> <span style={FlexStyle}>
<IconFont style={{ fontSize: '0.85em' }} /> <IconFont style={{ fontSize: '0.85em' }} />
<span <span
style={{ style={{
@ -50,7 +50,7 @@ export const TextColor: React.FC<{ editor: Editor }> = ({ editor }) => {
backgroundColor: color, backgroundColor: color,
}} }}
></span> ></span>
</div> </span>
} }
disabled={isTitleActive} disabled={isTitleActive}
/> />