refactor: improve mobile ux

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

View File

@ -36,7 +36,7 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 0,
'@typescript-eslint/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,

View File

@ -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}
>

View File

@ -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);
}
}

View File

@ -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} />
)}
<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 />
<>
{isMobile ? null : (
<>
{actions}
<Divider />
</>
)}
<Theme />
<User />
</Space>
</>
}
/>
{isMobile && <div className={styles.mobileToolbar}>{actions}</div>}
</header>
<main className={styles.contentWrap}>
<DataRender

View File

@ -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);
}
}

View File

@ -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>
);
};

View File

@ -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={
<>

View File

@ -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;
> 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);
.semi-navigation-header-list-outer {
flex: 1;
height: calc(100% - 64px);
}
&.selected {
.semi-navigation-footer {
height: 64px;
}
}
}
.selected {
color: var(--semi-color-primary);
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);
}
}
}

View File

@ -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>

View File

@ -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,22 +59,9 @@ export const EmojiPicker: React.FC<IProps> = ({ onSelectEmoji, children }) => {
[onSelectEmoji]
);
useEffect(() => {
if (!visible) return;
emojiLocalStorageLRUCache.syncFromStorage();
setRecentUsed(emojiLocalStorageLRUCache.get() as string[]);
}, [visible]);
return (
<Popover
showArrow
zIndex={10000}
trigger="click"
position="bottomLeft"
visible={visible}
onVisibleChange={toggleVisible}
content={
<div className={styles.wrap}>
const content = useMemo(
() => (
<div className={styles.wrap} style={{ paddingBottom: isMobile ? 24 : 0 }}>
{renderedList.map((item, index) => {
return (
<div key={item.title} className={styles.sectionWrap}>
@ -90,9 +79,45 @@ export const EmojiPicker: React.FC<IProps> = ({ onSelectEmoji, children }) => {
);
})}
</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}
</Popover>
)}
</span>
);
};

View File

@ -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,12 +158,7 @@ const MessageBox = () => {
});
}, [unreadMsgs, readMessage]);
return (
<Dropdown
position="bottomRight"
trigger="click"
content={
<div style={{ width: 300, padding: '16px 16px 0' }}>
const content = (
<Tabs
type="line"
size="small"
@ -195,9 +199,9 @@ const MessageBox = () => {
/>
</TabPane>
</Tabs>
</div>
}
>
);
const btn = (
<Button
type="tertiary"
theme="borderless"
@ -210,8 +214,36 @@ const MessageBox = () => {
<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>
)}
</span>
);
};

View File

@ -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();

View File

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

View File

@ -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)}>

View File

@ -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>

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -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);

View File

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

View File

@ -1,7 +1,7 @@
import React from 'react';
import { 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,17 +66,63 @@ 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>
{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
mode="horizontal"
style={{ overflow: 'auto' }}
header={
<Space>
<LogoImage />
{windowSize.width >= 890 && <LogoText />}
{width >= 890 && <LogoText />}
</Space>
}
selectedKeys={[pathname || '/']}
@ -104,6 +137,7 @@ export const RouterHeader: React.FC = () => {
</Space>
}
></Nav>
)}
</SemiHeader>
);
};

View File

@ -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,15 +14,10 @@ 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
@ -54,8 +50,7 @@ export const Recent = () => {
<Text size="small" type="tertiary">
{doc.createUser && doc.createUser.name} {' '}
<LocaleTime date={doc.updatedAt} timeago />
{doc.createUser && doc.createUser.name} <LocaleTime date={doc.updatedAt} timeago />
</Text>
</div>
</div>
@ -76,6 +71,37 @@ export const Recent = () => {
/>
</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>
}
>
@ -86,5 +112,6 @@ export const Recent = () => {
</Space>
</span>
</Dropdown>
</span>
);
};

View File

@ -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,22 +12,13 @@ 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 (
<Dropdown
trigger="click"
spacing={16}
content={
<div
style={{
width: 300,
paddingBottom: 8,
}}
>
<>
{currentWiki && (
<>
<div className={styles.titleWrap}>
@ -145,6 +136,38 @@ export const Wiki = () => {
</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"
spacing={16}
content={
<div
style={{
width: 300,
paddingBottom: 8,
}}
>
<WikiContent />
</div>
}
>

View File

@ -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,18 +80,16 @@ const colors = [
];
export const ColorPicker: React.FC<{
onSetColor;
title?: string;
onSetColor: (arg: string) => void;
disabled?: boolean;
}> = ({ children, onSetColor, disabled = false }) => {
if (disabled) return <span style={{ display: 'inline-block' }}>{children}</span>;
}> = ({ children, title = '颜色管理', onSetColor, disabled = false }) => {
const { isMobile } = useWindowSize();
const [visible, toggleVisible] = useToggle(false);
return (
<Dropdown
zIndex={10000}
trigger="click"
position={'bottomLeft'}
render={
<div style={{ padding: '8px 0' }}>
const content = useMemo(
() => (
<div style={{ padding: isMobile ? '0 0 24px' : '12px 16px' }}>
<div className={styles.emptyWrap} onClick={() => onSetColor(null)}>
<span></span>
<Text></Text>
@ -105,9 +105,35 @@ export const ColorPicker: React.FC<{
})}
</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>
</Dropdown>
)}
</span>
);
};

View File

@ -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;

View File

@ -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} />
)}
</>
);
});

View File

@ -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 {

View File

@ -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}
/>

View File

@ -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" />

View File

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

View File

@ -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

View File

@ -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,15 +63,8 @@ export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
};
}, [editor]);
return (
<Popover
showArrow
zIndex={10000}
trigger="click"
position="bottomRight"
onVisibleChange={onVisibleChange}
content={
<div>
const content = (
<div style={{ paddingBottom: isMobile ? 24 : 0 }}>
<div style={{ marginBottom: 12 }}>
<Text type="tertiary"></Text>
<Input
@ -94,13 +98,43 @@ export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
</Space>
</div>
</div>
}
>
<span>
);
const btn = (
<Tooltip content="查找替换">
<Button theme={'borderless'} type="tertiary" icon={<IconSearchReplace />} />
<Button theme={'borderless'} type="tertiary" icon={<IconSearchReplace />} onMouseDown={openModalOnMobile} />
</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>
)}
</span>
);
};

View File

@ -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}
/>