mirror of https://github.com/fantasticit/think.git
refactor: improve mobile ux
This commit is contained in:
parent
af358c1e04
commit
ef61f1bdf3
|
@ -36,7 +36,7 @@ module.exports = {
|
|||
'@typescript-eslint/explicit-module-boundary-types': 0,
|
||||
'@typescript-eslint/ban-types': 0,
|
||||
'react-hooks/rules-of-hooks': 2,
|
||||
'react-hooks/exhaustive-deps': 1,
|
||||
'react-hooks/exhaustive-deps': 2,
|
||||
'react/prop-types': 0,
|
||||
'testing-library/no-unnecessary-act': 0,
|
||||
'react/react-in-jsx-scope': 0,
|
||||
|
|
|
@ -128,7 +128,6 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId })
|
|||
visible={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={() => toggleVisible(false)}
|
||||
maskClosable={false}
|
||||
style={{ maxWidth: '96vw' }}
|
||||
footer={null}
|
||||
>
|
||||
|
|
|
@ -8,12 +8,14 @@
|
|||
> header {
|
||||
position: relative;
|
||||
z-index: 110;
|
||||
height: 60px;
|
||||
background-color: var(--semi-color-nav-bg);
|
||||
user-select: none;
|
||||
|
||||
> div {
|
||||
.mobileToolbar {
|
||||
padding: 12px 16px;
|
||||
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 }) => {
|
||||
const { width: windowWith } = useWindowSize();
|
||||
const { width: windowWith, isMobile } = useWindowSize();
|
||||
const { width, fontSize } = useDocumentStyle();
|
||||
const editorWrapClassNames = useMemo(() => {
|
||||
return width === 'standardWidth' ? styles.isStandardWidth : styles.isFullWidth;
|
||||
|
@ -44,6 +44,20 @@ export const DocumentEditor: React.FC<IProps> = ({ 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(() => {
|
||||
event.on(CHANGE_DOCUMENT_TITLE, setTitle);
|
||||
|
||||
|
@ -61,7 +75,7 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
|
|||
header={
|
||||
<>
|
||||
<Tooltip content="返回" position="bottom">
|
||||
<Button onClick={goback} icon={<IconChevronLeft />} style={{ marginRight: 16 }} />
|
||||
<Button onMouseDown={goback} icon={<IconChevronLeft />} style={{ marginRight: 16 }} />
|
||||
</Tooltip>
|
||||
<DataRender
|
||||
loading={docAuthLoading}
|
||||
|
@ -83,22 +97,19 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
|
|||
</>
|
||||
}
|
||||
footer={
|
||||
<Space>
|
||||
{document && authority.readable && (
|
||||
<DocumentCollaboration key="collaboration" wikiId={document.wikiId} documentId={documentId} />
|
||||
<>
|
||||
{isMobile ? null : (
|
||||
<>
|
||||
{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 />
|
||||
<Divider />
|
||||
<User />
|
||||
</Space>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{isMobile && <div className={styles.mobileToolbar}>{actions}</div>}
|
||||
</header>
|
||||
<main className={styles.contentWrap}>
|
||||
<DataRender
|
||||
|
|
|
@ -38,4 +38,22 @@
|
|||
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%',
|
||||
backgroundColor: '#0077fa',
|
||||
color: '#fff',
|
||||
bottom: 100,
|
||||
right: 16,
|
||||
bottom: 70,
|
||||
transform: 'translateY(-50px)',
|
||||
};
|
||||
|
||||
|
@ -42,7 +43,7 @@ interface IProps {
|
|||
|
||||
export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
||||
const [container, setContainer] = useState<HTMLDivElement>();
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
const { width: windowWidth, isMobile } = useWindowSize();
|
||||
const { width, fontSize } = useDocumentStyle();
|
||||
const editorWrapClassNames = useMemo(() => {
|
||||
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`);
|
||||
}, [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;
|
||||
|
||||
return (
|
||||
|
@ -89,35 +116,14 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
|||
ellipsis={{
|
||||
showTooltip: { opts: { content: document.title, style: { wordBreak: 'break-all' } } },
|
||||
}}
|
||||
style={{ width: ~~(windowWidth / 4) }}
|
||||
style={{ width: isMobile ? windowWidth - 100 : ~~(windowWidth / 4) }}
|
||||
>
|
||||
{document.title}
|
||||
</Text>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
<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>
|
||||
}
|
||||
footer={isMobile ? <></> : actions}
|
||||
></Nav>
|
||||
</Header>
|
||||
<Layout className={styles.contentWrap}>
|
||||
|
@ -153,12 +159,12 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
|||
<CommentEditor documentId={document.id} />
|
||||
</div>
|
||||
)}
|
||||
{authority && authority.editable && container && (
|
||||
{!isMobile && authority && authority.editable && container && (
|
||||
<BackTop style={EditBtnStyle} onClick={gotoEdit} target={() => container} visibilityHeight={200}>
|
||||
<IconEdit />
|
||||
</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>
|
||||
</Layout>
|
||||
{isMobile && <div className={styles.mobileToolbar}>{actions}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -40,14 +40,12 @@ export const DocumentShare: React.FC<IProps> = ({ documentId, render }) => {
|
|||
{isPublic ? '分享中' : '分享'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
title={isPublic ? '关闭分享' : '开启分享'}
|
||||
okText={isPublic ? '关闭分享' : '开启分享'}
|
||||
visible={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={() => toggleVisible(false)}
|
||||
maskClosable={false}
|
||||
style={{ maxWidth: '96vw' }}
|
||||
footer={
|
||||
<>
|
||||
|
|
|
@ -21,51 +21,23 @@
|
|||
margin: 0 -24px;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
> aside {
|
||||
width: 240px;
|
||||
height: 100%;
|
||||
padding: 12px 0;
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid var(--semi-color-border);
|
||||
overflow: auto;
|
||||
:global {
|
||||
.semi-navigation-inner {
|
||||
flex-direction: column;
|
||||
|
||||
> ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
.semi-navigation-header-list-outer {
|
||||
flex: 1;
|
||||
height: calc(100% - 64px);
|
||||
}
|
||||
|
||||
> 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 {
|
||||
color: var(--semi-color-primary);
|
||||
background-color: var(--semi-color-primary-light-default);
|
||||
}
|
||||
.semi-navigation-footer {
|
||||
height: 64px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> 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);
|
||||
}
|
||||
.selected {
|
||||
color: var(--semi-color-primary);
|
||||
background-color: var(--semi-color-primary-light-default);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { useEditor, EditorContent } from '@tiptap/react';
|
||||
import cls from 'classnames';
|
||||
|
@ -16,6 +16,7 @@ interface IProps {
|
|||
onSelect?: (data) => void;
|
||||
}
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
const { Title } = Typography;
|
||||
|
||||
export const DocumentVersion: React.FC<IProps> = ({ documentId, onSelect }) => {
|
||||
|
@ -105,28 +106,40 @@ export const DocumentVersion: React.FC<IProps> = ({ documentId, onSelect }) => {
|
|||
error={error}
|
||||
empty={!loading && !data.length}
|
||||
normalContent={() => (
|
||||
<div className={styles.contentWrap}>
|
||||
<aside>
|
||||
<ul>
|
||||
<Layout className={styles.contentWrap}>
|
||||
<Sider style={{ backgroundColor: 'var(--semi-color-bg-1)' }}>
|
||||
<Nav
|
||||
style={{ maxWidth: 200, height: '100%' }}
|
||||
bodyStyle={{ height: '100%' }}
|
||||
selectedKeys={[selectedVersion]}
|
||||
footer={{
|
||||
collapseButton: true,
|
||||
}}
|
||||
>
|
||||
{data.map(({ version, data }) => {
|
||||
return (
|
||||
<li
|
||||
<Nav.Item
|
||||
key={version}
|
||||
itemKey={version}
|
||||
className={cls(selectedVersion && selectedVersion.version === version && styles.selected)}
|
||||
text={<LocaleTime date={+version} />}
|
||||
onClick={() => select({ version, data })}
|
||||
>
|
||||
<LocaleTime date={+version} />
|
||||
</li>
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</aside>
|
||||
<main>
|
||||
<div className={cls('container', styles.editorWrap)}>
|
||||
</Nav>
|
||||
</Sider>
|
||||
<Content
|
||||
style={{
|
||||
padding: 16,
|
||||
backgroundColor: 'var(--semi-color-bg-0)',
|
||||
}}
|
||||
>
|
||||
<div className={'container'}>
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</Content>
|
||||
</Layout>
|
||||
)}
|
||||
/>
|
||||
</Modal>
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
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 { createKeysLocalStorageLRUCache } from 'helpers/lru-cache';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import styles from './index.module.scss';
|
||||
import { useWindowSize } from 'hooks/use-window-size';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
|
@ -41,6 +42,7 @@ interface IProps {
|
|||
}
|
||||
|
||||
export const EmojiPicker: React.FC<IProps> = ({ onSelectEmoji, children }) => {
|
||||
const { isMobile } = useWindowSize();
|
||||
const [recentUsed, setRecentUsed] = useState([]);
|
||||
const [visible, toggleVisible] = useToggle(false);
|
||||
const renderedList = useMemo(
|
||||
|
@ -57,6 +59,30 @@ export const EmojiPicker: React.FC<IProps> = ({ onSelectEmoji, children }) => {
|
|||
[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(() => {
|
||||
if (!visible) return;
|
||||
emojiLocalStorageLRUCache.syncFromStorage();
|
||||
|
@ -64,35 +90,34 @@ export const EmojiPicker: React.FC<IProps> = ({ onSelectEmoji, children }) => {
|
|||
}, [visible]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
showArrow
|
||||
zIndex={10000}
|
||||
trigger="click"
|
||||
position="bottomLeft"
|
||||
visible={visible}
|
||||
onVisibleChange={toggleVisible}
|
||||
content={
|
||||
<div className={styles.wrap}>
|
||||
{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>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Popover>
|
||||
<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}
|
||||
</Popover>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
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 { useAllMessages, useReadMessages, useUnreadMessages } from 'data/message';
|
||||
import { EmptyBoxIllustration } from 'illustrations/empty-box';
|
||||
|
@ -9,6 +9,8 @@ import { Empty } from 'components/empty';
|
|||
import { Placeholder } from './placeholder';
|
||||
import styles from './index.module.scss';
|
||||
import { useUser } from 'data/user';
|
||||
import { useWindowSize } from 'hooks/use-window-size';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
|
||||
const { Text } = Typography;
|
||||
const PAGE_SIZE = 6;
|
||||
|
@ -84,6 +86,8 @@ const MessagesRender = ({ messageData, loading, error, onClick = null, page = 1,
|
|||
};
|
||||
|
||||
const MessageBox = () => {
|
||||
const { isMobile } = useWindowSize();
|
||||
const [visible, toggleVisible] = useToggle(false);
|
||||
const { data: allMsgs, loading: allLoading, error: allError, page: allPage, setPage: allSetPage } = useAllMessages();
|
||||
const {
|
||||
data: readMsgs,
|
||||
|
@ -109,6 +113,11 @@ const MessageBox = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const openModalOnMobile = useCallback(() => {
|
||||
if (!isMobile) return;
|
||||
toggleVisible(true);
|
||||
}, [isMobile, toggleVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!unreadMsgs || !unreadMsgs.total) return;
|
||||
|
||||
|
@ -149,69 +158,92 @@ const MessageBox = () => {
|
|||
});
|
||||
}, [unreadMsgs, readMessage]);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
position="bottomRight"
|
||||
trigger="click"
|
||||
content={
|
||||
<div style={{ width: 300, padding: '16px 16px 0' }}>
|
||||
<Tabs
|
||||
type="line"
|
||||
size="small"
|
||||
tabBarExtraContent={
|
||||
unreadMsgs && unreadMsgs.total > 0 ? (
|
||||
<Text type="quaternary" onClick={clearAll} style={{ cursor: 'pointer' }}>
|
||||
全部已读
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<TabPane tab="未读" itemKey="unread">
|
||||
<MessagesRender
|
||||
messageData={unreadMsgs}
|
||||
loading={unreadLoading}
|
||||
error={unreadError}
|
||||
onClick={readMessage}
|
||||
page={unreadPage}
|
||||
onPageChange={unreadSetPage}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab="已读" itemKey="read">
|
||||
<MessagesRender
|
||||
messageData={readMsgs}
|
||||
loading={readLoading}
|
||||
error={readError}
|
||||
page={readPage}
|
||||
onPageChange={readSetPage}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab="全部" itemKey="all">
|
||||
<MessagesRender
|
||||
messageData={allMsgs}
|
||||
loading={allLoading}
|
||||
error={allError}
|
||||
page={allPage}
|
||||
onPageChange={allSetPage}
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
const content = (
|
||||
<Tabs
|
||||
type="line"
|
||||
size="small"
|
||||
tabBarExtraContent={
|
||||
unreadMsgs && unreadMsgs.total > 0 ? (
|
||||
<Text type="quaternary" onClick={clearAll} style={{ cursor: 'pointer' }}>
|
||||
全部已读
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<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 />
|
||||
)
|
||||
}
|
||||
></Button>
|
||||
</Dropdown>
|
||||
<TabPane tab="未读" itemKey="unread">
|
||||
<MessagesRender
|
||||
messageData={unreadMsgs}
|
||||
loading={unreadLoading}
|
||||
error={unreadError}
|
||||
onClick={readMessage}
|
||||
page={unreadPage}
|
||||
onPageChange={unreadSetPage}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab="已读" itemKey="read">
|
||||
<MessagesRender
|
||||
messageData={readMsgs}
|
||||
loading={readLoading}
|
||||
error={readError}
|
||||
page={readPage}
|
||||
onPageChange={readSetPage}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab="全部" itemKey="all">
|
||||
<MessagesRender
|
||||
messageData={allMsgs}
|
||||
loading={allLoading}
|
||||
error={allError}
|
||||
page={allPage}
|
||||
onPageChange={allSetPage}
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
);
|
||||
|
||||
const 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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
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 { useTheme } from 'hooks/use-theme';
|
||||
import { Tooltip } from 'components/tooltip';
|
||||
|
||||
export const Theme = () => {
|
||||
const { theme, toggle } = useTheme();
|
||||
|
|
|
@ -21,7 +21,7 @@ export const Tooltip: React.FC<IProps> = ({ content, hideOnClick = false, positi
|
|||
onMouseLeave={() => {
|
||||
toggleVisible(false);
|
||||
}}
|
||||
onClick={() => {
|
||||
onMouseMove={() => {
|
||||
hideOnClick && toggleVisible(false);
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -25,7 +25,7 @@ export const User: React.FC = () => {
|
|||
<>
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
position="bottomLeft"
|
||||
position="bottomRight"
|
||||
render={
|
||||
<Dropdown.Menu style={{ width: 160 }}>
|
||||
<Dropdown.Item onClick={() => toggleVisible(true)}>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
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 { useQuery } from 'hooks/use-query';
|
||||
import { WikiCreator } from 'components/wiki/create';
|
||||
|
@ -11,6 +12,7 @@ interface IProps {
|
|||
}
|
||||
|
||||
export const WikiOrDocumentCreator: React.FC<IProps> = ({ onCreateDocument, children }) => {
|
||||
const { isMobile } = useWindowSize();
|
||||
const { wikiId, docId } = useQuery<{ wikiId?: string; docId?: string }>();
|
||||
const [visible, toggleVisible] = useToggle(false);
|
||||
const [createDocumentModalVisible, toggleCreateDocumentModalVisible] = useToggle(false);
|
||||
|
@ -25,7 +27,9 @@ export const WikiOrDocumentCreator: React.FC<IProps> = ({ onCreateDocument, chil
|
|||
</Dropdown.Menu>
|
||||
}
|
||||
>
|
||||
{children || (
|
||||
{children || isMobile ? (
|
||||
<Button type="primary" theme="solid" icon={<IconPlus />} size="small" />
|
||||
) : (
|
||||
<Button type="primary" theme="solid" icon={<IconChevronDown />} iconPosition="right">
|
||||
新建
|
||||
</Button>
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
.treeInnerWrap {
|
||||
:global {
|
||||
.semi-tree-option-list-block .semi-tree-option-selected {
|
||||
font-weight: 600;
|
||||
color: var(--semi-color-primary);
|
||||
background-color: var(--semi-color-primary-light-default);
|
||||
}
|
||||
|
@ -55,6 +56,7 @@
|
|||
}
|
||||
|
||||
&.isActive {
|
||||
font-weight: 600;
|
||||
color: var(--semi-color-primary);
|
||||
background-color: var(--semi-color-primary-light-default);
|
||||
}
|
||||
|
@ -90,7 +92,7 @@
|
|||
|
||||
.title {
|
||||
overflow: hidden;
|
||||
color: var(--semi-color-text-0);
|
||||
color: inherit;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
|
|
|
@ -77,6 +77,7 @@ export const Tree = ({ data, docAsLink, getDocLink, parentIds, activeId, isShare
|
|||
ellipsis={{
|
||||
showTooltip: { opts: { content: label, style: { wordBreak: 'break-all' }, position: 'right' } },
|
||||
}}
|
||||
style={{ color: 'inherit' }}
|
||||
>
|
||||
{label}
|
||||
</Typography.Text>
|
||||
|
|
|
@ -66,7 +66,7 @@ export const useDragableWidth = () => {
|
|||
setStorage(key, nextWidth);
|
||||
}
|
||||
mutate();
|
||||
}, [mutate, currentWidth, minWidth]);
|
||||
}, [mutate, currentWidth, minWidth, maxWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
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 {
|
||||
width: number | undefined;
|
||||
height: number | undefined;
|
||||
isMobile: boolean;
|
||||
}
|
||||
|
||||
const PC_MOBILE_CRITICAL_WIDTH = 765;
|
||||
|
||||
export function useWindowSize(): Size {
|
||||
const [windowSize, setWindowSize] = useState<Size>({
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
isMobile: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -16,6 +20,7 @@ export function useWindowSize(): Size {
|
|||
setWindowSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
isMobile: window.innerWidth <= PC_MOBILE_CRITICAL_WIDTH,
|
||||
});
|
||||
}
|
||||
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 { 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 Link from 'next/link';
|
||||
import { User } from 'components/user';
|
||||
import { WikiOrDocumentCreator } from 'components/wiki-or-document-creator';
|
||||
import { LogoImage, LogoText } from 'components/logo';
|
||||
|
@ -9,19 +9,18 @@ import { Theme } from 'components/theme';
|
|||
import { Message } from 'components/message';
|
||||
import { Search } from 'components/search';
|
||||
import { useWindowSize } from 'hooks/use-window-size';
|
||||
import { Recent } from './recent';
|
||||
import { Wiki } from './wiki';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import { Recent, RecentModal } from './recent';
|
||||
import { Wiki, WikiModal } from './wiki';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const { Header: SemiHeader } = SemiLayout;
|
||||
const { Text } = Typography;
|
||||
|
||||
const menus = [
|
||||
{
|
||||
itemKey: '/',
|
||||
text: (
|
||||
<Link href="/">
|
||||
<a>主页</a>
|
||||
</Link>
|
||||
),
|
||||
text: '主页',
|
||||
onClick: () => {
|
||||
Router.push({
|
||||
pathname: `/`,
|
||||
|
@ -38,11 +37,7 @@ const menus = [
|
|||
},
|
||||
{
|
||||
itemKey: '/star',
|
||||
text: (
|
||||
<Link href="/star">
|
||||
<a>收藏</a>
|
||||
</Link>
|
||||
),
|
||||
text: '收藏',
|
||||
onClick: () => {
|
||||
Router.push({
|
||||
pathname: `/star`,
|
||||
|
@ -51,11 +46,7 @@ const menus = [
|
|||
},
|
||||
{
|
||||
itemKey: '/template',
|
||||
text: (
|
||||
<Link href="/template">
|
||||
<a>模板</a>
|
||||
</Link>
|
||||
),
|
||||
text: '模板',
|
||||
onClick: () => {
|
||||
Router.push({
|
||||
pathname: `/template`,
|
||||
|
@ -64,11 +55,7 @@ const menus = [
|
|||
},
|
||||
{
|
||||
itemKey: '/find',
|
||||
text: (
|
||||
<Link href="/find">
|
||||
<a>发现</a>
|
||||
</Link>
|
||||
),
|
||||
text: '发现',
|
||||
onClick: () => {
|
||||
Router.push({
|
||||
pathname: `/find`,
|
||||
|
@ -79,22 +66,47 @@ const menus = [
|
|||
|
||||
export const RouterHeader: React.FC = () => {
|
||||
const { pathname } = useRouter();
|
||||
const windowSize = useWindowSize();
|
||||
const { width, isMobile } = useWindowSize();
|
||||
const [recentModalVisible, toggleRecentModalVisible] = useToggle(false);
|
||||
const [wikiModalVisible, toggleWikiModalVisible] = useToggle(false);
|
||||
|
||||
return (
|
||||
<SemiHeader>
|
||||
<Nav
|
||||
mode="horizontal"
|
||||
style={{ overflow: 'auto' }}
|
||||
header={
|
||||
{isMobile ? (
|
||||
<div className={styles.mobileHeader}>
|
||||
<Space>
|
||||
<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>
|
||||
}
|
||||
selectedKeys={[pathname || '/']}
|
||||
items={menus}
|
||||
footer={
|
||||
|
||||
<Space>
|
||||
<WikiOrDocumentCreator />
|
||||
<Search />
|
||||
|
@ -102,8 +114,30 @@ export const RouterHeader: React.FC = () => {
|
|||
<Theme />
|
||||
<User />
|
||||
</Space>
|
||||
}
|
||||
></Nav>
|
||||
</div>
|
||||
) : (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
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 { useRecentDocuments } from 'data/document';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import { Empty } from 'components/empty';
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { LocaleTime } from 'components/locale-time';
|
||||
|
@ -13,78 +14,104 @@ import styles from './index.module.scss';
|
|||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const Recent = () => {
|
||||
export const RecentDocs = () => {
|
||||
const { data: recentDocs, loading, error } = useRecentDocuments();
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
spacing={16}
|
||||
content={
|
||||
<div style={{ width: 300, padding: '16px 16px 0' }}>
|
||||
<Tabs type="line" size="small">
|
||||
<TabPane tab="文档" itemKey="docs">
|
||||
<DataRender
|
||||
loading={loading}
|
||||
loadingContent={<Placeholder />}
|
||||
error={error}
|
||||
normalContent={() => {
|
||||
return (
|
||||
<div className={styles.itemsWrap} style={{ margin: '0 -16px' }}>
|
||||
{recentDocs.length ? (
|
||||
recentDocs.map((doc) => {
|
||||
return (
|
||||
<div className={styles.itemWrap} key={doc.id}>
|
||||
<Link
|
||||
href={{
|
||||
pathname: '/wiki/[wikiId]/document/[documentId]',
|
||||
query: {
|
||||
wikiId: doc.wikiId,
|
||||
documentId: doc.id,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<a className={styles.item}>
|
||||
<div className={styles.leftWrap}>
|
||||
<IconDocumentFill style={{ marginRight: 12 }} />
|
||||
<div>
|
||||
<Text ellipsis={{ showTooltip: true }} style={{ width: 180 }}>
|
||||
{doc.title}
|
||||
</Text>
|
||||
<Tabs type="line" size="small">
|
||||
<TabPane tab="文档" itemKey="docs">
|
||||
<DataRender
|
||||
loading={loading}
|
||||
loadingContent={<Placeholder />}
|
||||
error={error}
|
||||
normalContent={() => {
|
||||
return (
|
||||
<div className={styles.itemsWrap} style={{ margin: '0 -16px' }}>
|
||||
{recentDocs.length ? (
|
||||
recentDocs.map((doc) => {
|
||||
return (
|
||||
<div className={styles.itemWrap} key={doc.id}>
|
||||
<Link
|
||||
href={{
|
||||
pathname: '/wiki/[wikiId]/document/[documentId]',
|
||||
query: {
|
||||
wikiId: doc.wikiId,
|
||||
documentId: doc.id,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<a className={styles.item}>
|
||||
<div className={styles.leftWrap}>
|
||||
<IconDocumentFill style={{ marginRight: 12 }} />
|
||||
<div>
|
||||
<Text ellipsis={{ showTooltip: true }} style={{ width: 180 }}>
|
||||
{doc.title}
|
||||
</Text>
|
||||
|
||||
<Text size="small" type="tertiary">
|
||||
创建者:
|
||||
{doc.createUser && doc.createUser.name} •{' '}
|
||||
<LocaleTime date={doc.updatedAt} timeago />
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.rightWrap}>
|
||||
<DocumentStar documentId={doc.id} />
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
<Text size="small" type="tertiary">
|
||||
创建者:
|
||||
{doc.createUser && doc.createUser.name} • <LocaleTime date={doc.updatedAt} timeago />
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Empty message="最近访问的文档会出现在此处" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<Space>
|
||||
最近
|
||||
<IconChevronDown />
|
||||
</Space>
|
||||
</span>
|
||||
</Dropdown>
|
||||
<div className={styles.rightWrap}>
|
||||
<DocumentStar documentId={doc.id} />
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Empty message="最近访问的文档会出现在此处" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</TabPane>
|
||||
</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>
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<Space>
|
||||
最近
|
||||
<IconChevronDown />
|
||||
</Space>
|
||||
</span>
|
||||
</Dropdown>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
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 { useStaredWikis, useWikiDetail } from 'data/wiki';
|
||||
import { Empty } from 'components/empty';
|
||||
|
@ -12,11 +12,150 @@ import styles from './index.module.scss';
|
|||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const Wiki = () => {
|
||||
const WikiContent = () => {
|
||||
const { query } = useRouter();
|
||||
const { data: starWikis, loading, error, refresh: refreshStarWikis } = useStaredWikis();
|
||||
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 (
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
|
@ -28,123 +167,7 @@ export const Wiki = () => {
|
|||
paddingBottom: 8,
|
||||
}}
|
||||
>
|
||||
{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>
|
||||
<WikiContent />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import React from 'react';
|
||||
import { Dropdown, Typography } from '@douyinfe/semi-ui';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Dropdown, Typography, Modal } from '@douyinfe/semi-ui';
|
||||
import styles from './style.module.scss';
|
||||
import { useWindowSize } from 'hooks/use-window-size';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
|
@ -78,36 +80,60 @@ const colors = [
|
|||
];
|
||||
|
||||
export const ColorPicker: React.FC<{
|
||||
onSetColor;
|
||||
title?: string;
|
||||
onSetColor: (arg: string) => void;
|
||||
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>;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
zIndex={10000}
|
||||
trigger="click"
|
||||
position={'bottomLeft'}
|
||||
render={
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<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>
|
||||
}
|
||||
>
|
||||
<span style={{ display: 'inline-block' }}>{children}</span>
|
||||
</Dropdown>
|
||||
<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>
|
||||
</Dropdown>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
.emptyWrap {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
padding: 8px 10px;
|
||||
width: 240px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--semi-color-fill-1);
|
||||
|
@ -11,9 +12,9 @@
|
|||
> span:first-child {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 8px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 0 8px 0 1px;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 2px;
|
||||
|
||||
|
@ -34,8 +35,8 @@
|
|||
.colorWrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 256px;
|
||||
padding: 8px;
|
||||
width: 240px;
|
||||
margin-top: 8px;
|
||||
|
||||
.colorItem {
|
||||
display: flex;
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
import React, { useEffect, forwardRef, useImperativeHandle, useRef, useMemo } from 'react';
|
||||
import { Toast, BackTop } from '@douyinfe/semi-ui';
|
||||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||
import cls from 'classnames';
|
||||
import { debounce } from 'helpers/debounce';
|
||||
import { useNetwork } from 'hooks/use-network';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import { useWindowSize } from 'hooks/use-window-size';
|
||||
import { LogoName } from 'components/logo';
|
||||
import { Banner } from 'components/banner';
|
||||
import { useEditor, EditorContent } from '../../react';
|
||||
import { Collaboration } from 'tiptap/core/extensions/collaboration';
|
||||
import { CollaborationCursor } from 'tiptap/core/extensions/collaboration-cursor';
|
||||
import { getRandomColor } from 'helpers/color';
|
||||
import { useEditor, EditorContent } from '../../react';
|
||||
import { CollaborationKit } from '../kit';
|
||||
import { MenuBar } from './menubar';
|
||||
import { ICollaborationEditorProps, ProviderStatus } from './type';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
type IProps = Pick<
|
||||
ICollaborationEditorProps,
|
||||
|
@ -25,6 +28,7 @@ type IProps = Pick<
|
|||
export const EditorInstance = forwardRef((props: IProps, ref) => {
|
||||
const { hocuspocusProvider, editable, user, onTitleUpdate, status, menubar, renderInEditorPortal } = props;
|
||||
const $mainContainer = useRef<HTMLDivElement>();
|
||||
const { isMobile } = useWindowSize();
|
||||
const { online } = useNetwork();
|
||||
const [created, toggleCreated] = useToggle(false);
|
||||
const editor = useEditor(
|
||||
|
@ -98,7 +102,7 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
|
|||
<Banner type="warning" description="您没有编辑权限,暂不能编辑该文档。" closeable={false} />
|
||||
)}
|
||||
{menubar && (
|
||||
<header>
|
||||
<header className={cls(isMobile && styles.mobileToolbar)}>
|
||||
<MenuBar editor={editor} />
|
||||
</header>
|
||||
)}
|
||||
|
@ -107,7 +111,9 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
|
|||
{protals}
|
||||
</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;
|
||||
border-bottom: 1px solid var(--semi-color-border);
|
||||
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 {
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Title } from 'tiptap/core/extensions/title';
|
|||
import { ColorPicker } from 'tiptap/components/color-picker';
|
||||
|
||||
const FlexStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
@ -33,16 +33,16 @@ export const BackgroundColor: React.FC<{ editor: Editor }> = ({ editor }) => {
|
|||
);
|
||||
|
||||
return (
|
||||
<ColorPicker onSetColor={setBackgroundColor} disabled={isTitleActive}>
|
||||
<ColorPicker title="背景色" onSetColor={setBackgroundColor} disabled={isTitleActive}>
|
||||
<Tooltip content="背景色">
|
||||
<Button
|
||||
theme={editor.isActive('textStyle') ? 'light' : 'borderless'}
|
||||
type={'tertiary'}
|
||||
icon={
|
||||
<div style={FlexStyle}>
|
||||
<span style={FlexStyle}>
|
||||
<IconMark />
|
||||
<span style={{ backgroundColor, width: 12, height: 2 }}></span>
|
||||
</div>
|
||||
</span>
|
||||
}
|
||||
disabled={isTitleActive}
|
||||
/>
|
||||
|
|
|
@ -33,7 +33,14 @@ export const CountdownSettingModal: React.FC<IProps> = ({ editor }) => {
|
|||
}, [editor, toggleVisible]);
|
||||
|
||||
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.Input labelWidth={72} label="标题" field="title" required />
|
||||
<Form.DatePicker labelWidth={72} style={{ width: '100%' }} label="截止日期" field="date" type="dateTime" />
|
||||
|
|
|
@ -71,6 +71,7 @@ export const IframeBubbleMenu = ({ editor }) => {
|
|||
>
|
||||
<Modal
|
||||
title="编辑链接"
|
||||
style={{ maxWidth: '96vw' }}
|
||||
visible={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={() => toggleVisible(false)}
|
||||
|
|
|
@ -45,7 +45,14 @@ export const LinkSettingModal: React.FC<IProps> = ({ editor }) => {
|
|||
}, [editor, toggleVisible]);
|
||||
|
||||
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.Input label="文本" field="text" placeholder="请输入文本"></Form.Input>
|
||||
<Form.Input
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
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 { useWindowSize } from 'hooks/use-window-size';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import { Tooltip } from 'components/tooltip';
|
||||
import { IconSearchReplace } from 'components/icons';
|
||||
import { SearchNReplace } from 'tiptap/core/extensions/search';
|
||||
|
@ -8,17 +10,26 @@ import { SearchNReplace } from 'tiptap/core/extensions/search';
|
|||
const { Text } = Typography;
|
||||
|
||||
export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||
const { isMobile } = useWindowSize();
|
||||
const [visible, toggleVisible] = useToggle(false);
|
||||
const [currentIndex, setCurrentIndex] = useState(-1);
|
||||
const [results, setResults] = useState([]);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [replaceValue, setReplaceValue] = useState('');
|
||||
|
||||
const onVisibleChange = useCallback((visible) => {
|
||||
const openModalOnMobile = useCallback(() => {
|
||||
if (!isMobile) return;
|
||||
toggleVisible(true);
|
||||
}, [isMobile, toggleVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
setSearchValue('');
|
||||
setReplaceValue('');
|
||||
setCurrentIndex(-1);
|
||||
setResults([]);
|
||||
}
|
||||
}, []);
|
||||
}, [visible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editor && editor.commands && editor.commands.setSearchTerm) {
|
||||
|
@ -52,55 +63,78 @@ export const Search: React.FC<{ 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 (
|
||||
<Popover
|
||||
showArrow
|
||||
zIndex={10000}
|
||||
trigger="click"
|
||||
position="bottomRight"
|
||||
onVisibleChange={onVisibleChange}
|
||||
content={
|
||||
<div>
|
||||
<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>
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<Tooltip content="查找替换">
|
||||
<Button theme={'borderless'} type="tertiary" icon={<IconSearchReplace />} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
</Popover>
|
||||
<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>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,7 +12,7 @@ import { ColorPicker } from 'tiptap/components/color-picker';
|
|||
type Color = { color: string };
|
||||
|
||||
const FlexStyle = {
|
||||
display: 'flex',
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
} as React.CSSProperties;
|
||||
|
@ -35,13 +35,13 @@ export const TextColor: React.FC<{ editor: Editor }> = ({ editor }) => {
|
|||
);
|
||||
|
||||
return (
|
||||
<ColorPicker onSetColor={setColor} disabled={isTitleActive}>
|
||||
<ColorPicker title="文本色" onSetColor={setColor} disabled={isTitleActive}>
|
||||
<Tooltip content="文本色">
|
||||
<Button
|
||||
theme={isTextStyleActive ? 'light' : 'borderless'}
|
||||
type={'tertiary'}
|
||||
icon={
|
||||
<div style={FlexStyle}>
|
||||
<span style={FlexStyle}>
|
||||
<IconFont style={{ fontSize: '0.85em' }} />
|
||||
<span
|
||||
style={{
|
||||
|
@ -50,7 +50,7 @@ export const TextColor: React.FC<{ editor: Editor }> = ({ editor }) => {
|
|||
backgroundColor: color,
|
||||
}}
|
||||
></span>
|
||||
</div>
|
||||
</span>
|
||||
}
|
||||
disabled={isTitleActive}
|
||||
/>
|
||||
|
|
Loading…
Reference in New Issue