diff --git a/packages/client/package.json b/packages/client/package.json index e1c7f94f..00739fe0 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -55,11 +55,14 @@ "@tiptap/react": "^2.0.0-beta.107", "@tiptap/suggestion": "^2.0.0-beta.90", "axios": "^0.25.0", + "buffer-image-size": "^0.6.4", "classnames": "^2.3.1", "clone": "^2.1.2", "cross-env": "^7.0.3", "deep-equal": "^2.0.5", + "docx": "^7.3.0", "dompurify": "^2.3.5", + "downloadjs": "^1.4.7", "interactjs": "^1.10.11", "katex": "^0.15.2", "kity": "^2.0.4", diff --git a/packages/client/src/components/document/actions/index.tsx b/packages/client/src/components/document/actions/index.tsx index 3540e47d..20c7ab4e 100644 --- a/packages/client/src/components/document/actions/index.tsx +++ b/packages/client/src/components/document/actions/index.tsx @@ -1,9 +1,11 @@ -import { IconArticle, IconBranch, IconHistory, IconMore, IconPlus, IconStar } from '@douyinfe/semi-icons'; +import { IconArticle, IconBranch, IconExport, IconHistory, IconMore, IconPlus, IconStar } from '@douyinfe/semi-icons'; import { Button, Dropdown, Space, Typography } from '@douyinfe/semi-ui'; import { ButtonProps } from '@douyinfe/semi-ui/button/Button'; +import { IDocument } from '@think/domains'; import cls from 'classnames'; import { DocumentCreator } from 'components/document/create'; import { DocumentDeletor } from 'components/document/delete'; +import { DocumentExporter } from 'components/document/export'; import { DocumentLinkCopyer } from 'components/document/link'; import { DocumentShare } from 'components/document/share'; import { DocumentStar } from 'components/document/star'; @@ -17,6 +19,7 @@ import styles from './index.module.scss'; interface IProps { wikiId: string; documentId: string; + document?: IDocument; hoverVisible?: boolean; onStar?: () => void; onCreate?: () => void; @@ -34,6 +37,7 @@ export const DocumentActions: React.FC = ({ wikiId, documentId, hoverVisible, + document, onStar, onCreate, onDelete, @@ -179,6 +183,24 @@ export const DocumentActions: React.FC = ({ /> )} + {document && ( + { + return ( + toggleVisible(true)}> + + + + 文档导出 + + + + ); + }} + /> + )} + void }) => React.ReactNode; +} + +export const DocumentExporter: React.FC = ({ document, render }) => { + const { isMobile } = IsOnMobile.useHook(); + const [visible, toggleVisible] = useToggle(false); + + const editor = useMemo(() => { + return createEditor({ + editable: false, + extensions: AllExtensions, + content: '', + }); + }, []); + + const exportMarkdown = useCallback(() => { + const md = prosemirrorToMarkdown({ content: editor.state.doc.slice(0).content }); + download(md, `${document.title}.md`, 'text/plain'); + }, [document, editor]); + + const exportJSON = useCallback(() => { + download(safeJSONStringify(editor.getJSON()), `${document.title}.json`, 'text/plain'); + }, [document, editor]); + + const exportWord = useCallback(() => { + prosemirrorToDocx(editor.view, editor.state).then((buffer) => { + download(buffer, `${document.title}.docx`); + }); + }, [document, editor]); + + const exportPDF = useCallback(() => { + printEditorContent(editor.view); + }, [editor]); + + const content = useMemo( + () => ( +
+ +
+
+ +
+
+ Markdown +
+
+ .md +
+
+ +
+
+ +
+
+ JSON +
+
+ .json +
+
+ +
+
+ + + +
+
+ Word +
+
+ .docx +
+
+ +
+
+ + + +
+
+ PDF +
+
+ .pdf +
+
+
+
+ ), + [exportMarkdown, exportJSON, exportWord, exportPDF] + ); + + const btn = useMemo( + () => + render ? ( + render({ toggleVisible }) + ) : ( + + ), + [render, toggleVisible] + ); + + useEffect(() => { + const c = safeJSONParse(document && document.content); + const json = c.default || c; + editor.commands.setContent(json); + }, [editor, document]); + + return ( + <> + {isMobile ? ( + <> + + {content} + + {btn} + + ) : ( + {content}} + > + {btn} + + )} + + ); +}; diff --git a/packages/client/src/components/document/export/pdf.ts b/packages/client/src/components/document/export/pdf.ts new file mode 100644 index 00000000..c50b782e --- /dev/null +++ b/packages/client/src/components/document/export/pdf.ts @@ -0,0 +1,57 @@ +import { EditorView } from 'prosemirror-view'; + +function printHtml(dom: Element) { + const style: string = Array.from(document.querySelectorAll('style, link')).reduce( + (str, style) => str + style.outerHTML, + '' + ); + + const content: string = style + dom.outerHTML; + + const iframe: HTMLIFrameElement = document.createElement('iframe'); + iframe.id = 'el-tiptap-iframe'; + iframe.setAttribute('style', 'position: absolute; width: 0; height: 0; top: -10px; left: -10px;'); + document.body.appendChild(iframe); + + const frameWindow = iframe.contentWindow; + const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document); + + if (doc) { + doc.open(); + doc.write(content); + doc.close(); + } + + if (frameWindow) { + iframe.onload = function () { + try { + setTimeout(() => { + frameWindow.focus(); + try { + if (!frameWindow.document.execCommand('print', false)) { + frameWindow.print(); + } + } catch (e) { + frameWindow.print(); + } + frameWindow.close(); + }, 10); + } catch (err) { + console.error(err); + } + + setTimeout(function () { + document.body.removeChild(iframe); + }, 100); + }; + } +} + +export function printEditorContent(view: EditorView) { + const editorContent = view.dom.closest('.ProseMirror'); + if (editorContent) { + printHtml(editorContent); + return true; + } + return false; +} diff --git a/packages/client/src/components/document/reader/index.tsx b/packages/client/src/components/document/reader/index.tsx index 4330905e..7224d237 100644 --- a/packages/client/src/components/document/reader/index.tsx +++ b/packages/client/src/components/document/reader/index.tsx @@ -74,7 +74,7 @@ export const DocumentReader: React.FC = ({ documentId }) => {