mirror of https://github.com/fantasticit/think.git
Merge pull request #32 from fantasticit/feat/mobile
This commit is contained in:
commit
acf2a0cc8d
|
@ -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,
|
||||||
|
|
|
@ -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}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
<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 />
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Theme />
|
||||||
<User />
|
<User />
|
||||||
</Space>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{isMobile && <div className={styles.mobileToolbar}>{actions}</div>}
|
||||||
</header>
|
</header>
|
||||||
<main className={styles.contentWrap}>
|
<main className={styles.contentWrap}>
|
||||||
<DataRender
|
<DataRender
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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={
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -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 {
|
|
||||||
width: 100%;
|
|
||||||
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 {
|
.semi-navigation-footer {
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> main {
|
|
||||||
padding: 24px 0;
|
|
||||||
overflow: auto;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,22 +59,9 @@ export const EmojiPicker: React.FC<IProps> = ({ onSelectEmoji, children }) => {
|
||||||
[onSelectEmoji]
|
[onSelectEmoji]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const content = useMemo(
|
||||||
if (!visible) return;
|
() => (
|
||||||
emojiLocalStorageLRUCache.syncFromStorage();
|
<div className={styles.wrap} style={{ paddingBottom: isMobile ? 24 : 0 }}>
|
||||||
setRecentUsed(emojiLocalStorageLRUCache.get() as string[]);
|
|
||||||
}, [visible]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover
|
|
||||||
showArrow
|
|
||||||
zIndex={10000}
|
|
||||||
trigger="click"
|
|
||||||
position="bottomLeft"
|
|
||||||
visible={visible}
|
|
||||||
onVisibleChange={toggleVisible}
|
|
||||||
content={
|
|
||||||
<div className={styles.wrap}>
|
|
||||||
{renderedList.map((item, index) => {
|
{renderedList.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<div key={item.title} className={styles.sectionWrap}>
|
<div key={item.title} className={styles.sectionWrap}>
|
||||||
|
@ -90,9 +79,45 @@ export const EmojiPicker: React.FC<IProps> = ({ onSelectEmoji, children }) => {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
}
|
),
|
||||||
|
[isMobile, renderedList, selectEmoji]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) return;
|
||||||
|
emojiLocalStorageLRUCache.syncFromStorage();
|
||||||
|
setRecentUsed(emojiLocalStorageLRUCache.get() as string[]);
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{isMobile ? (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
centered
|
||||||
|
title="表情"
|
||||||
|
visible={visible}
|
||||||
|
footer={null}
|
||||||
|
onCancel={() => toggleVisible(false)}
|
||||||
|
style={{ maxWidth: '96vw' }}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</Modal>
|
||||||
|
<span onMouseDown={() => toggleVisible(true)}>{children}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Popover
|
||||||
|
showArrow
|
||||||
|
zIndex={10000}
|
||||||
|
trigger="click"
|
||||||
|
position="bottomLeft"
|
||||||
|
visible={visible}
|
||||||
|
onVisibleChange={toggleVisible}
|
||||||
|
content={content}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Popover>
|
</Popover>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,12 +158,7 @@ const MessageBox = () => {
|
||||||
});
|
});
|
||||||
}, [unreadMsgs, readMessage]);
|
}, [unreadMsgs, readMessage]);
|
||||||
|
|
||||||
return (
|
const content = (
|
||||||
<Dropdown
|
|
||||||
position="bottomRight"
|
|
||||||
trigger="click"
|
|
||||||
content={
|
|
||||||
<div style={{ width: 300, padding: '16px 16px 0' }}>
|
|
||||||
<Tabs
|
<Tabs
|
||||||
type="line"
|
type="line"
|
||||||
size="small"
|
size="small"
|
||||||
|
@ -195,9 +199,9 @@ const MessageBox = () => {
|
||||||
/>
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
);
|
||||||
}
|
|
||||||
>
|
const btn = (
|
||||||
<Button
|
<Button
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
theme="borderless"
|
theme="borderless"
|
||||||
|
@ -210,8 +214,36 @@ const MessageBox = () => {
|
||||||
<IconMessage />
|
<IconMessage />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
></Button>
|
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>
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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)}>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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,17 +66,63 @@ 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>
|
||||||
|
{isMobile ? (
|
||||||
|
<div className={styles.mobileHeader}>
|
||||||
|
<Space>
|
||||||
|
<LogoImage />
|
||||||
|
<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>
|
||||||
|
<WikiOrDocumentCreator />
|
||||||
|
<Search />
|
||||||
|
<Message />
|
||||||
|
<Theme />
|
||||||
|
<User />
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<Nav
|
<Nav
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
style={{ overflow: 'auto' }}
|
style={{ overflow: 'auto' }}
|
||||||
header={
|
header={
|
||||||
<Space>
|
<Space>
|
||||||
<LogoImage />
|
<LogoImage />
|
||||||
{windowSize.width >= 890 && <LogoText />}
|
{width >= 890 && <LogoText />}
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
selectedKeys={[pathname || '/']}
|
selectedKeys={[pathname || '/']}
|
||||||
|
@ -104,6 +137,7 @@ export const RouterHeader: React.FC = () => {
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
></Nav>
|
></Nav>
|
||||||
|
)}
|
||||||
</SemiHeader>
|
</SemiHeader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,15 +14,10 @@ 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
|
|
||||||
trigger="click"
|
|
||||||
spacing={16}
|
|
||||||
content={
|
|
||||||
<div style={{ width: 300, padding: '16px 16px 0' }}>
|
|
||||||
<Tabs type="line" size="small">
|
<Tabs type="line" size="small">
|
||||||
<TabPane tab="文档" itemKey="docs">
|
<TabPane tab="文档" itemKey="docs">
|
||||||
<DataRender
|
<DataRender
|
||||||
|
@ -54,8 +50,7 @@ export const Recent = () => {
|
||||||
|
|
||||||
<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>
|
||||||
|
@ -76,6 +71,37 @@ export const Recent = () => {
|
||||||
/>
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -86,5 +112,6 @@ export const Recent = () => {
|
||||||
</Space>
|
</Space>
|
||||||
</span>
|
</span>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,22 +12,13 @@ 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 (
|
return (
|
||||||
<Dropdown
|
<>
|
||||||
trigger="click"
|
|
||||||
spacing={16}
|
|
||||||
content={
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: 300,
|
|
||||||
paddingBottom: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{currentWiki && (
|
{currentWiki && (
|
||||||
<>
|
<>
|
||||||
<div className={styles.titleWrap}>
|
<div className={styles.titleWrap}>
|
||||||
|
@ -145,6 +136,38 @@ export const Wiki = () => {
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</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 (
|
||||||
|
<Dropdown
|
||||||
|
trigger="click"
|
||||||
|
spacing={16}
|
||||||
|
content={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 300,
|
||||||
|
paddingBottom: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<WikiContent />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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,18 +80,16 @@ 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 }) => {
|
||||||
if (disabled) return <span style={{ display: 'inline-block' }}>{children}</span>;
|
const { isMobile } = useWindowSize();
|
||||||
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
|
|
||||||
return (
|
const content = useMemo(
|
||||||
<Dropdown
|
() => (
|
||||||
zIndex={10000}
|
<div style={{ padding: isMobile ? '0 0 24px' : '12px 16px' }}>
|
||||||
trigger="click"
|
|
||||||
position={'bottomLeft'}
|
|
||||||
render={
|
|
||||||
<div style={{ padding: '8px 0' }}>
|
|
||||||
<div className={styles.emptyWrap} onClick={() => onSetColor(null)}>
|
<div className={styles.emptyWrap} onClick={() => onSetColor(null)}>
|
||||||
<span></span>
|
<span></span>
|
||||||
<Text>无颜色</Text>
|
<Text>无颜色</Text>
|
||||||
|
@ -105,9 +105,35 @@ export const ColorPicker: React.FC<{
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
),
|
||||||
|
[onSetColor, isMobile]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (disabled) return <span style={{ display: 'inline-block' }}>{children}</span>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{isMobile ? (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
centered
|
||||||
|
title={title}
|
||||||
|
visible={visible}
|
||||||
|
footer={null}
|
||||||
|
onCancel={() => toggleVisible(false)}
|
||||||
|
style={{ maxWidth: '96vw', width: 288 }}
|
||||||
>
|
>
|
||||||
|
{content}
|
||||||
|
</Modal>
|
||||||
|
<span style={{ display: 'inline-block' }} onMouseDown={() => toggleVisible(true)}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Dropdown zIndex={10000} trigger="click" position={'bottomLeft'} render={content}>
|
||||||
<span style={{ display: 'inline-block' }}>{children}</span>
|
<span style={{ display: 'inline-block' }}>{children}</span>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,15 +63,8 @@ export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
};
|
};
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
return (
|
const content = (
|
||||||
<Popover
|
<div style={{ paddingBottom: isMobile ? 24 : 0 }}>
|
||||||
showArrow
|
|
||||||
zIndex={10000}
|
|
||||||
trigger="click"
|
|
||||||
position="bottomRight"
|
|
||||||
onVisibleChange={onVisibleChange}
|
|
||||||
content={
|
|
||||||
<div>
|
|
||||||
<div style={{ marginBottom: 12 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<Text type="tertiary">查找</Text>
|
<Text type="tertiary">查找</Text>
|
||||||
<Input
|
<Input
|
||||||
|
@ -94,13 +98,43 @@ export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
);
|
||||||
>
|
|
||||||
<span>
|
const btn = (
|
||||||
<Tooltip content="查找替换">
|
<Tooltip content="查找替换">
|
||||||
<Button theme={'borderless'} type="tertiary" icon={<IconSearchReplace />} />
|
<Button theme={'borderless'} type="tertiary" icon={<IconSearchReplace />} onMouseDown={openModalOnMobile} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</span>
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{isMobile ? (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
centered
|
||||||
|
title="查找替换"
|
||||||
|
visible={visible}
|
||||||
|
footer={null}
|
||||||
|
onCancel={() => toggleVisible(false)}
|
||||||
|
style={{ maxWidth: '96vw' }}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</Modal>
|
||||||
|
{btn}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Popover
|
||||||
|
showArrow
|
||||||
|
zIndex={10000}
|
||||||
|
trigger="click"
|
||||||
|
position="bottomRight"
|
||||||
|
visible={visible}
|
||||||
|
onVisibleChange={toggleVisible}
|
||||||
|
content={content}
|
||||||
|
>
|
||||||
|
<span>{btn}</span>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
Loading…
Reference in New Issue