diff --git a/packages/client/package.json b/packages/client/package.json
index 6035a2a..b1c5337 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -89,6 +89,7 @@
"react": "17.0.2",
"react-countdown": "^2.3.2",
"react-dom": "17.0.2",
+ "react-full-screen": "^1.1.1",
"react-helmet": "^6.1.0",
"react-lazy-load-image-component": "^1.5.4",
"react-pdf": "^5.7.2",
diff --git a/packages/client/src/components/document/fullscreen/index.module.scss b/packages/client/src/components/document/fullscreen/index.module.scss
new file mode 100644
index 0000000..7ff86b1
--- /dev/null
+++ b/packages/client/src/components/document/fullscreen/index.module.scss
@@ -0,0 +1,123 @@
+.wrap {
+ width: 100vw;
+ height: 100vh;
+ overflow: auto;
+ background-color: var(--semi-color-bg-0);
+}
+
+.fullscreenContainer {
+ position: fixed;
+ background: var(--semi-color-bg-0);
+
+ .show {
+ z-index: 2;
+ opacity: 1;
+ }
+
+ .hidden {
+ z-index: -1;
+ opacity: 0;
+ }
+
+ .fullscreenContent {
+ position: relative;
+ min-width: 100%;
+ min-height: 100%;
+
+ .header {
+ position: fixed;
+ z-index: 2;
+ display: flex;
+ width: 100%;
+ height: 60px;
+ padding: 0 24px;
+ background-color: var(--semi-color-bg-0);
+ opacity: 0.9;
+ align-items: center;
+ }
+
+ .content {
+ position: relative;
+ z-index: 1;
+ width: auto;
+ height: 100vh;
+ min-height: 680px;
+ padding: 0 120px;
+ margin: 0 auto;
+ overflow: auto;
+ letter-spacing: 0.05em;
+ background: var(--semi-color-bg-1);
+ transition: width 0.3s linear, padding 0.3s linear;
+
+ .title {
+ display: table-cell;
+ height: 100vh;
+ font-size: 4rem;
+ line-height: 4.6rem;
+ color: var(--semi-color-text-0);
+ text-align: left;
+ word-wrap: break-word;
+ vertical-align: middle;
+ overflow-wrap: break-word;
+ }
+
+ .node-title.is-empty {
+ display: none !important;
+ }
+ }
+ }
+}
+
+.fullScreenToolbar {
+ position: fixed;
+ bottom: 40px;
+ left: 50%;
+ z-index: 2;
+ padding: 12px;
+ background-color: rgb(var(--semi-grey-9));
+ border-radius: var(--semi-border-radius-medium);
+ opacity: 0;
+ transform: translateX(-50%);
+ transition: opacity 0.3s ease-in-out;
+
+ &:hover {
+ opacity: 1;
+ transition: opacity 0.3s ease-in-out;
+ }
+
+ .selected {
+ background-color: rgb(var(--semi-grey-8)) !important;
+ }
+
+ .customButton {
+ display: flex;
+ height: 32px;
+ min-width: 32px;
+ padding: 0 8px;
+ font-size: 14px;
+ color: rgb(var(--semi-grey-0));
+ cursor: pointer;
+ background: none;
+ border: none;
+ border-radius: 2px;
+ outline: none;
+ align-items: center;
+
+ &:hover {
+ background-color: rgb(var(--semi-grey-8));
+ }
+ }
+
+ .divider {
+ width: 1px;
+ height: 1em;
+ background: rgb(var(--semi-grey-0));
+ }
+}
+
+.drawingCursor {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 999999;
+}
diff --git a/packages/client/src/components/document/fullscreen/index.tsx b/packages/client/src/components/document/fullscreen/index.tsx
new file mode 100644
index 0000000..c14eb2d
--- /dev/null
+++ b/packages/client/src/components/document/fullscreen/index.tsx
@@ -0,0 +1,122 @@
+import { IconShrinkScreenStroked } from '@douyinfe/semi-icons';
+import { Button, Space, Tooltip, Typography } from '@douyinfe/semi-ui';
+import { EditorContent, useEditor } from '@tiptap/react';
+import cls from 'classnames';
+import { IconFullscreen } from 'components/icons/IconFullscreen';
+import { IconPencil } from 'components/icons/IconPencil';
+import { safeJSONParse } from 'helpers/json';
+import { useDrawingCursor } from 'hooks/use-cursor';
+import { useToggle } from 'hooks/use-toggle';
+import React, { useCallback, useEffect } from 'react';
+import { FullScreen, useFullScreenHandle } from 'react-full-screen';
+import { CollaborationKit } from 'tiptap/editor';
+
+import styles from './index.module.scss';
+
+// 控制器
+const FullscreenController = ({ handle: fullscreenHandler, isDrawing, toggleDrawing }) => {
+ const startDrawing = useCallback(() => {
+ toggleDrawing(!isDrawing);
+ }, [isDrawing, toggleDrawing]);
+
+ const close = useCallback(() => {
+ fullscreenHandler.exit();
+ toggleDrawing(false);
+ }, [fullscreenHandler, toggleDrawing]);
+ return (
+
+ );
+};
+
+// 画笔
+const DrawingCursor = ({ isDrawing }) => {
+ useDrawingCursor(isDrawing);
+ return isDrawing && ;
+};
+
+interface IProps {
+ data?: any;
+}
+
+// 全屏按钮
+export const DocumentFullscreen: React.FC = ({ data }) => {
+ const fullscreenHandler = useFullScreenHandle();
+ const [visible, toggleVisible] = useToggle(false);
+ const [isDrawing, toggleDrawing] = useToggle(false);
+
+ const editor = useEditor({
+ editable: false,
+ extensions: CollaborationKit,
+ content: { type: 'doc', content: [] },
+ });
+
+ const startPowerpoint = () => {
+ toggleVisible(true);
+ fullscreenHandler.enter();
+ };
+
+ const fullscreenChange = useCallback(
+ (state) => {
+ if (!state) {
+ toggleVisible(false);
+ toggleDrawing(false);
+ }
+ },
+ [toggleVisible, toggleDrawing]
+ );
+
+ useEffect(() => {
+ if (!editor) return;
+ const docJSON = safeJSONParse(data.content, { default: {} }).default;
+ docJSON.content = docJSON.content.filter((item) => item.type !== 'title');
+ editor.commands.setContent(docJSON);
+ }, [editor, data]);
+
+ const { Title } = Typography;
+ return (
+
+ }
+ theme="borderless"
+ onClick={(e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ startPowerpoint();
+ }}
+ />
+
+
+ {visible && (
+
+
+
+ {data.title}
+
+
+
+
{data.title || '未命名文档'}
+
+
+
+
+
+ )}
+
+
+ );
+};
diff --git a/packages/client/src/components/document/reader/index.tsx b/packages/client/src/components/document/reader/index.tsx
index 6343c93..b039a02 100644
--- a/packages/client/src/components/document/reader/index.tsx
+++ b/packages/client/src/components/document/reader/index.tsx
@@ -19,6 +19,7 @@ import { createPortal } from 'react-dom';
import { CollaborationEditor } from 'tiptap/editor';
import { DocumentActions } from '../actions';
+import { DocumentFullscreen } from '../fullscreen';
import { Author } from './author';
import styles from './index.module.scss';
@@ -73,6 +74,7 @@ export const DocumentReader: React.FC = ({ documentId }) => {
documentId={documentId}
/>
)}
+ {document && !isMobile && }
{document && (
= ({ documentId }) => {
);
- }, [document, documentId, readable, editable, gotoEdit]);
+ }, [document, documentId, readable, editable, gotoEdit, isMobile]);
return (
diff --git a/packages/client/src/components/document/reader/public/index.tsx b/packages/client/src/components/document/reader/public/index.tsx
index 2e04227..d4d6298 100644
--- a/packages/client/src/components/document/reader/public/index.tsx
+++ b/packages/client/src/components/document/reader/public/index.tsx
@@ -23,6 +23,7 @@ import styles from './index.module.scss';
import Router from 'next/router';
import { useRouterQuery } from 'hooks/use-router-query';
import { IDocument, IWiki } from '@think/domains';
+import { DocumentFullscreen } from 'components/document/fullscreen';
const { Header } = Layout;
const { Text } = Typography;
@@ -148,6 +149,7 @@ export const DocumentPublicReader: React.FC
= ({ documentId, hideLogo =
}
footer={
+ {!isMobile && }
} onClick={toPublicWikiOrDocumentURL} />
diff --git a/packages/client/src/components/icons/IconFullscreen.tsx b/packages/client/src/components/icons/IconFullscreen.tsx
new file mode 100644
index 0000000..8337943
--- /dev/null
+++ b/packages/client/src/components/icons/IconFullscreen.tsx
@@ -0,0 +1,18 @@
+import { Icon } from '@douyinfe/semi-ui';
+
+export const IconFullscreen: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
+ return (
+
+
+
+ }
+ />
+ );
+};
diff --git a/packages/client/src/components/icons/IconPencil.tsx b/packages/client/src/components/icons/IconPencil.tsx
new file mode 100644
index 0000000..d74807d
--- /dev/null
+++ b/packages/client/src/components/icons/IconPencil.tsx
@@ -0,0 +1,26 @@
+import { Icon } from '@douyinfe/semi-ui';
+
+export const IconPencil: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
+ return (
+
+
+
+ }
+ />
+ );
+};
diff --git a/packages/client/src/hooks/use-cursor.tsx b/packages/client/src/hooks/use-cursor.tsx
new file mode 100644
index 0000000..73b2afe
--- /dev/null
+++ b/packages/client/src/hooks/use-cursor.tsx
@@ -0,0 +1,107 @@
+import { useEffect } from 'react';
+
+let timer;
+let cursor = { x: 0, y: 0 };
+let particles = [];
+
+const applyProperties = (target, properties) => {
+ for (const key in properties) {
+ target.style[key] = properties[key];
+ }
+};
+
+// Particles
+class Particle {
+ lifeSpan: number;
+ initialStyles: any;
+ velocity: { x: number; y: number };
+ position: { x: number; y: number };
+ element: HTMLElement;
+ constructor() {
+ this.lifeSpan = 20; //ms
+ this.initialStyles = {
+ 'position': 'fixed',
+ 'display': 'block',
+ 'width': '12px',
+ 'height': '12px',
+ 'border-radius': '50%',
+ 'pointer-events': 'none',
+ 'backgroundColor': '#D61C11',
+ 'will-change': 'transform',
+ 'z-index': '9999999',
+ };
+ }
+ init(x, y) {
+ this.position = { x: x - 6, y: y - 6 };
+
+ this.element = document.createElement('span');
+ applyProperties(this.element, this.initialStyles);
+ this.update();
+
+ document.querySelector('.drawing-cursor').appendChild(this.element);
+ }
+ update() {
+ this.lifeSpan--;
+
+ this.element.style.transform =
+ 'translate3d(' + this.position.x + 'px,' + this.position.y + 'px, 0) scale(' + this.lifeSpan / 20 + ')';
+ this.element.style.opacity = '0.5';
+ }
+ destroy() {
+ this.element.parentNode.removeChild(this.element);
+ }
+}
+
+const addParticle = (x, y) => {
+ const particle = new Particle();
+ particle.init(x, y);
+ particles.push(particle);
+};
+
+const onMousemove = (e) => {
+ cursor.x = e.clientX;
+ cursor.y = e.clientY;
+
+ addParticle(cursor.x, cursor.y);
+};
+
+const updateParticles = () => {
+ // Updated particles
+ for (let i = 0; i < particles.length; i++) {
+ particles[i].update();
+ }
+ // Remove dead particles
+ for (let i = particles.length - 1; i >= 0; i--) {
+ if (particles[i].lifeSpan < 0) {
+ particles[i].destroy();
+ particles.splice(i, 1);
+ }
+ }
+};
+const loop = () => {
+ timer = requestAnimationFrame(loop);
+ updateParticles();
+};
+
+const destroyDrawingCursor = () => {
+ document.removeEventListener('mousemove', onMousemove);
+ cancelAnimationFrame(timer);
+ cursor = { x: 0, y: 0 };
+ particles = [];
+};
+const mountDrawingCursor = () => {
+ document.querySelector('.drawing-cursor').innerHTML = '';
+ document.addEventListener('mousemove', onMousemove);
+ loop();
+};
+
+export const useDrawingCursor = (isDrawing) => {
+ useEffect(() => {
+ if (isDrawing) {
+ mountDrawingCursor();
+ }
+ return () => {
+ destroyDrawingCursor();
+ };
+ }, [isDrawing]);
+};
diff --git a/packages/client/src/tiptap/core/styles/title.scss b/packages/client/src/tiptap/core/styles/title.scss
index b00b51e..59860d9 100644
--- a/packages/client/src/tiptap/core/styles/title.scss
+++ b/packages/client/src/tiptap/core/styles/title.scss
@@ -9,18 +9,16 @@
&::before {
position: absolute;
- bottom: 0;
+ top: 0;
height: 0;
color: var(--semi-color-text-0);
pointer-events: none;
content: attr(data-placeholder);
- transform: translateY(-4.2em);
}
&.is-editable {
&::before {
color: #aaa;
- transform: translateY(-1.7em);
}
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6e3a08f..53c8611 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -135,6 +135,7 @@ importers:
react: 17.0.2
react-countdown: ^2.3.2
react-dom: 17.0.2
+ react-full-screen: ^1.1.1
react-helmet: ^6.1.0
react-lazy-load-image-component: ^1.5.4
react-pdf: ^5.7.2
@@ -234,6 +235,7 @@ importers:
react: 17.0.2
react-countdown: 2.3.2_sfoxds7t5ydpegc3knd667wn6m
react-dom: 17.0.2_react@17.0.2
+ react-full-screen: 1.1.1_react@17.0.2
react-helmet: 6.1.0_react@17.0.2
react-lazy-load-image-component: 1.5.4_sfoxds7t5ydpegc3knd667wn6m
react-pdf: 5.7.2_sfoxds7t5ydpegc3knd667wn6m
@@ -6392,6 +6394,10 @@ packages:
/fs.realpath/1.0.0:
resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=}
+ /fscreen/1.2.0:
+ resolution: {integrity: sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg==}
+ dev: false
+
/fsevents/2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -9740,6 +9746,16 @@ packages:
resolution: {integrity: sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==}
dev: false
+ /react-full-screen/1.1.1_react@17.0.2:
+ resolution: {integrity: sha512-xoEgkoTiN0dw9cjYYGViiMCBYbkS97BYb4bHPhQVWXj1UnOs8PZ1rPzpX+2HMhuvQV1jA5AF9GaRbO3fA5aZtg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ react: '>= 16.8.0'
+ dependencies:
+ fscreen: 1.2.0
+ react: 17.0.2
+ dev: false
+
/react-helmet/6.1.0_react@17.0.2:
resolution: {integrity: sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==}
peerDependencies: