From 9f395a10acb8d8f7d73f9de96567b091d04adb81 Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Thu, 21 May 2026 17:51:53 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BA=86=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E7=9A=84=E5=B7=A5=E5=85=B7=E6=A0=8F=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/FloatingToc/FloatingToc.css | 189 ++++++++++++++++++ .../components/FloatingToc/FloatingToc.jsx | 55 +++++ .../components/PDFViewer/VirtualPDFViewer.css | 32 ++- .../components/PDFViewer/VirtualPDFViewer.jsx | 118 +++++------ .../src/pages/Document/DocumentEditor.css | 18 +- frontend/src/pages/Document/DocumentPage.css | 98 ++------- frontend/src/pages/Document/DocumentPage.jsx | 127 +++++------- frontend/src/pages/Preview/FileSharePage.jsx | 92 ++++----- frontend/src/pages/Preview/PreviewPage.css | 100 ++------- .../src/pages/Preview/ProjectSharePage.jsx | 90 ++++----- 10 files changed, 505 insertions(+), 414 deletions(-) create mode 100644 frontend/src/components/FloatingToc/FloatingToc.css create mode 100644 frontend/src/components/FloatingToc/FloatingToc.jsx diff --git a/frontend/src/components/FloatingToc/FloatingToc.css b/frontend/src/components/FloatingToc/FloatingToc.css new file mode 100644 index 0000000..188573f --- /dev/null +++ b/frontend/src/components/FloatingToc/FloatingToc.css @@ -0,0 +1,189 @@ +.floating-toc { + position: fixed; + top: 50%; + right: 24px; + z-index: 30; + width: 48px; + height: 180px; + transform: translateY(-50%); + outline: none; +} + +.floating-toc-tab { + position: absolute; + top: 0; + right: 0; + width: 48px; + height: 180px; + border: 1px solid color-mix(in srgb, var(--border-color) 82%, var(--text-color-secondary)); + border-radius: 8px; + background: color-mix(in srgb, var(--card-bg) 92%, transparent); + color: var(--text-color-secondary); + box-shadow: 0 12px 30px rgba(15, 23, 42, 0.12); + backdrop-filter: blur(12px); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + cursor: default; + transition: opacity 0.18s ease, transform 0.18s ease, border-color 0.18s ease, color 0.18s ease; +} + +.floating-toc-tab span { + writing-mode: vertical-rl; + text-orientation: mixed; + font-size: 12px; + line-height: 1; + letter-spacing: 0; + white-space: nowrap; +} + +.floating-toc-panel { + position: absolute; + top: 0; + right: 0; + width: min(420px, calc(100vw - 320px)); + min-width: 300px; + max-height: min(58vh, 460px); + border: 1px solid color-mix(in srgb, var(--border-color) 78%, var(--text-color-secondary)); + border-radius: 8px; + background: color-mix(in srgb, var(--card-bg) 96%, transparent); + color: var(--text-color); + box-shadow: 0 22px 48px rgba(15, 23, 42, 0.18); + backdrop-filter: blur(16px); + opacity: 0; + pointer-events: none; + transform: translateX(10px) scale(0.98); + transform-origin: top right; + overflow: hidden; + transition: opacity 0.18s ease, transform 0.18s ease; +} + +.floating-toc:hover .floating-toc-tab, +.floating-toc:focus-within .floating-toc-tab { + opacity: 0; + transform: translateX(8px) scale(0.96); + pointer-events: none; +} + +.floating-toc:hover .floating-toc-panel, +.floating-toc:focus-within .floating-toc-panel { + opacity: 1; + pointer-events: auto; + transform: translateX(0) scale(1); +} + +.floating-toc-header { + min-height: 48px; + padding: 0 16px; + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + color: var(--text-color); + font-size: 14px; + font-weight: 600; +} + +.floating-toc-count { + min-width: 34px; + height: 22px; + padding: 0 8px; + border-radius: 999px; + background: var(--item-hover-bg); + color: var(--text-color-secondary); + font-size: 12px; + font-weight: 600; + line-height: 22px; + text-align: center; +} + +.floating-toc-content { + max-height: calc(min(58vh, 460px) - 49px); + overflow-y: auto; + overflow-x: hidden; + padding: 10px 8px 12px; +} + +.floating-toc-content .ant-anchor { + padding-left: 0; +} + +.floating-toc-content .ant-anchor::before { + display: none; +} + +.floating-toc-content .ant-anchor-ink { + display: none; +} + +.floating-toc-content .ant-anchor-link { + padding: 2px 0; +} + +.floating-toc-content .ant-anchor-link-title { + border-radius: 6px; + color: var(--text-color-secondary); + line-height: 1.45; + transition: background 0.16s ease, color 0.16s ease; +} + +.floating-toc-content .ant-anchor-link-title:hover, +.floating-toc-content .ant-anchor-link-active > .ant-anchor-link-title { + background: var(--item-hover-bg); + color: var(--link-color); +} + +.floating-toc-item { + min-height: 34px; + padding: 7px 8px; + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.floating-toc-item-icon { + flex: none; + font-size: 12px; + color: currentColor; + opacity: 0.72; +} + +.floating-toc-item-title { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 13px; +} + +.floating-toc-empty { + padding: 28px 12px; + color: var(--text-color-secondary); + text-align: center; + font-size: 13px; +} + +body.dark .floating-toc-tab, +body.dark .floating-toc-panel { + box-shadow: 0 22px 48px rgba(0, 0, 0, 0.38); +} + +@media (max-width: 1024px) { + .floating-toc { + right: 16px; + } + + .floating-toc-panel { + width: min(360px, calc(100vw - 300px)); + } +} + +@media (max-width: 768px) { + .floating-toc { + display: none; + } +} diff --git a/frontend/src/components/FloatingToc/FloatingToc.jsx b/frontend/src/components/FloatingToc/FloatingToc.jsx new file mode 100644 index 0000000..ea56046 --- /dev/null +++ b/frontend/src/components/FloatingToc/FloatingToc.jsx @@ -0,0 +1,55 @@ +import { Anchor } from 'antd' +import { FileTextOutlined, MenuOutlined } from '@ant-design/icons' +import './FloatingToc.css' + +export default function FloatingToc({ + items = [], + getContainer, + searchKeyword = '', + renderTitle, + className = '', +}) { + const anchorItems = items.map((item) => ({ + key: item.key, + href: item.href, + title: ( +
+ + + {renderTitle ? renderTitle(item, searchKeyword) : item.title} + +
+ ), + })) + + return ( + + ) +} diff --git a/frontend/src/components/PDFViewer/VirtualPDFViewer.css b/frontend/src/components/PDFViewer/VirtualPDFViewer.css index e95ef79..7230ccd 100644 --- a/frontend/src/components/PDFViewer/VirtualPDFViewer.css +++ b/frontend/src/components/PDFViewer/VirtualPDFViewer.css @@ -7,22 +7,42 @@ .pdf-toolbar { display: flex; - justify-content: space-between; + justify-content: flex-end; align-items: center; - padding: 12px 16px; - background: var(--card-bg); - border-bottom: 1px solid var(--border-color); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + gap: 16px; + min-width: 0; + background: transparent; z-index: 10; color: var(--text-color); } +.pdf-toolbar-scale { + min-width: 50px; + text-align: center; + color: var(--text-color); + font-size: 13px; +} + +.pdf-toolbar-divider { + width: 1px; + height: 18px; + background: var(--border-color); + display: inline-block; +} + .pdf-content { flex: 1; overflow: auto; position: relative; } +.virtual-pdf-viewer-container > .pdf-toolbar { + padding: 12px 16px; + background: var(--card-bg); + border-bottom: 1px solid var(--border-color); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + .pdf-virtual-list { background: var(--bg-color-secondary); } @@ -129,4 +149,4 @@ .react-pdf__Document { height: 100%; width: 100%; -} \ No newline at end of file +} diff --git a/frontend/src/components/PDFViewer/VirtualPDFViewer.jsx b/frontend/src/components/PDFViewer/VirtualPDFViewer.jsx index 5e3ed62..60b1558 100644 --- a/frontend/src/components/PDFViewer/VirtualPDFViewer.jsx +++ b/frontend/src/components/PDFViewer/VirtualPDFViewer.jsx @@ -1,4 +1,5 @@ import { useState, useMemo, useRef, useEffect, useCallback } from 'react' +import { createPortal } from 'react-dom' import { Document, Page, pdfjs } from 'react-pdf' import { Button, Space, InputNumber, message, Spin } from 'antd' import { @@ -16,7 +17,7 @@ import './VirtualPDFViewer.css' // 配置 PDF.js worker pdfjs.GlobalWorkerOptions.workerSrc = '/pdf-worker/pdf.worker.min.mjs' -function VirtualPDFViewer({ url, filename }) { +function VirtualPDFViewer({ url, filename, toolbarTarget }) { const [numPages, setNumPages] = useState(null) const [scale, setScale] = useState(1.0) const [pdfOriginalSize, setPdfOriginalSize] = useState({ width: 595, height: 842 }) // 默认 A4 @@ -159,65 +160,66 @@ function VirtualPDFViewer({ url, filename }) { document.body.removeChild(link) } + const toolbar = ( +
+ + + + {Math.round(scale * 100)}% + + + + + + + +
+ ) + return (
- {/* 工具栏 */} -
- - - - - - - - - - - {Math.round(scale * 100)}% - - - -
+ {toolbarTarget ? createPortal(toolbar, toolbarTarget) : toolbar} {/* PDF内容区 - 自定义虚拟滚动 */}
diff --git a/frontend/src/pages/Document/DocumentEditor.css b/frontend/src/pages/Document/DocumentEditor.css index de17308..df66b10 100644 --- a/frontend/src/pages/Document/DocumentEditor.css +++ b/frontend/src/pages/Document/DocumentEditor.css @@ -283,6 +283,12 @@ background-color: var(--item-hover-bg); } +/* The fixed editor layout does not use Bytemd's built-in sidebar modes. */ +.bytemd-toolbar-right .bytemd-toolbar-icon:nth-child(1), +.bytemd-toolbar-right .bytemd-toolbar-icon:nth-child(2) { + display: none; +} + /* 编辑和预览区域容器 */ .bytemd-body { flex: 1 !important; @@ -297,15 +303,10 @@ background-color: var(--bg-color); } -/* 编辑区域 - 固定50%宽度 */ +/* 编辑区域 - 默认分栏,保留 Bytemd 内联样式对仅编辑/仅预览的控制 */ .bytemd-editor { - width: 50% !important; - flex: 0 0 50% !important; - display: flex !important; - flex-direction: column !important; overflow: hidden; min-height: 0; - max-width: 50% !important; box-sizing: border-box; /* Added for consistent box model */ min-width: 0; @@ -314,16 +315,13 @@ border-right: 1px solid var(--border-color); } -/* 预览区域 - 固定50%宽度 */ +/* 预览区域 - 默认分栏,保留 Bytemd 内联样式对仅编辑/仅预览的控制 */ .bytemd-preview { - width: 50% !important; - flex: 0 0 50% !important; overflow-y: auto !important; overflow-x: hidden !important; padding: 16px; font-size: 14px; line-height: 1.8; - max-width: 50% !important; box-sizing: border-box; /* Added for consistent box model */ min-width: 0; diff --git a/frontend/src/pages/Document/DocumentPage.css b/frontend/src/pages/Document/DocumentPage.css index f586fbe..257db55 100644 --- a/frontend/src/pages/Document/DocumentPage.css +++ b/frontend/src/pages/Document/DocumentPage.css @@ -169,86 +169,6 @@ background: var(--bg-color); } -.docs-toc-sider { - border-left: 1px solid var(--border-color); - background: var(--bg-color-secondary) !important; - height: 100%; - overflow: hidden; - display: flex; - flex-direction: column; -} - -.docs-toc-sider .ant-layout-sider-children { - display: flex; - flex-direction: column; - height: 100%; - min-height: 0; -} - -.toc-header { - padding: 16px; - border-bottom: 1px solid var(--border-color); - display: flex; - justify-content: space-between; - align-items: center; - background: var(--header-bg); -} - -.toc-header h3 { - margin: 0; - font-size: 14px; - font-weight: 600; - color: var(--text-color); -} - -.toc-content { - flex: 1; - min-height: 0; - overflow-y: auto; - overflow-x: auto; - padding: 16px; -} - -.toc-content .ant-anchor { - display: block; - min-height: max-content; - padding-left: 0; - padding-bottom: 65px; - /* 给Anchor组件添加底部内边距,避免最后一项被遮挡 */ -} - -.toc-content .ant-anchor-link { - padding: 6px 0; -} - -.toc-content .ant-anchor-link-title { - font-size: 13px; - color: var(--text-color-secondary); - line-height: 1.5; - white-space: nowrap; -} - -.toc-content .ant-anchor-link-active>.ant-anchor-link-title { - color: var(--link-color); - font-weight: 500; -} - -.toc-empty { - color: var(--text-color-secondary); - text-align: center; - margin-top: 40px; - font-size: 13px; -} - -.toc-toggle-btn { - position: fixed; - right: 24px; - top: 50%; - transform: translateY(-50%); - z-index: 100; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); -} - .docs-content { height: 100%; overflow-y: auto; @@ -265,13 +185,15 @@ background: var(--header-bg); display: flex; align-items: center; + justify-content: space-between; + gap: 16px; } .docs-header-title { display: flex; align-items: center; min-width: 0; - max-width: 100%; + flex: 1; overflow: hidden; white-space: nowrap; gap: 0; @@ -301,6 +223,20 @@ white-space: nowrap; } +.docs-header-actions { + flex: none; +} + +.pdf-header-toolbar { + min-width: 0; + overflow-x: auto; + overflow-y: hidden; +} + +.pdf-header-toolbar .pdf-toolbar { + white-space: nowrap; +} + .docs-content-wrapper { max-width: 900px; margin: 0 auto; diff --git a/frontend/src/pages/Document/DocumentPage.jsx b/frontend/src/pages/Document/DocumentPage.jsx index 36ca8bf..2e37434 100644 --- a/frontend/src/pages/Document/DocumentPage.jsx +++ b/frontend/src/pages/Document/DocumentPage.jsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, useMemo } from 'react' import { useParams, useNavigate, useSearchParams } from 'react-router-dom' -import { Layout, Menu, Spin, FloatButton, Button, Tooltip, message, Anchor, Modal, Input, Space, Dropdown, Empty, Switch } from 'antd' -import { VerticalAlignTopOutlined, ShareAltOutlined, MenuFoldOutlined, MenuUnfoldOutlined, FileTextOutlined, FolderOutlined, FolderOpenOutlined, FilePdfOutlined, CopyOutlined, LockOutlined, CloudDownloadOutlined, CloudUploadOutlined, DownOutlined, SearchOutlined, ArrowLeftOutlined, MenuOutlined, ReloadOutlined } from '@ant-design/icons' +import { Layout, Menu, Spin, Button, Tooltip, message, Modal, Input, Space, Dropdown, Empty, Switch } from 'antd' +import { ShareAltOutlined, FileTextOutlined, FolderOutlined, FolderOpenOutlined, FilePdfOutlined, CopyOutlined, LockOutlined, CloudDownloadOutlined, CloudUploadOutlined, DownOutlined, ArrowLeftOutlined, ReloadOutlined, VerticalAlignTopOutlined } from '@ant-design/icons' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import rehypeRaw from 'rehype-raw' @@ -15,7 +15,7 @@ import { gitPull, gitPush, getGitRepos } from '@/api/project' import { getFileShareInfo, createOrUpdateFileShare, deleteFileShare } from '@/api/share' import { searchDocuments } from '@/api/search' import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer' -import DocFloatActions from '@/components/DocFloatActions/DocFloatActions' +import FloatingToc from '@/components/FloatingToc/FloatingToc' import Toast from '@/components/Toast/Toast' import ModeSwitch from '@/components/ModeSwitch/ModeSwitch' import './DocumentPage.css' @@ -45,7 +45,6 @@ function DocumentPage() { const [markdownContent, setMarkdownContent] = useState('') const [loading, setLoading] = useState(false) const [openKeys, setOpenKeys] = useState([]) - const [tocCollapsed, setTocCollapsed] = useState(false) const [tocItems, setTocItems] = useState([]) const [shareModalVisible, setShareModalVisible] = useState(false) const [shareInfo, setShareInfo] = useState(null) @@ -68,6 +67,7 @@ function DocumentPage() { const contentRef = useRef(null) const modeSwitchingRef = useRef(false) + const [pdfToolbarTarget, setPdfToolbarTarget] = useState(null) const getHeaderDisplay = (filePath) => { const resolvedPath = filePath || 'README.md' @@ -414,6 +414,22 @@ function DocumentPage() { } } + const scrollContentToTop = () => { + if (contentRef.current) { + contentRef.current.scrollTo({ top: 0, behavior: 'smooth' }) + } + } + + const handleExportMarkdownPDF = () => { + if (!selectedFile) return + let url = getExportPdfUrl(projectId, selectedFile) + const token = localStorage.getItem('access_token') + if (token) { + url += `&token=${encodeURIComponent(token)}` + } + window.open(url, '_blank') + } + // 解析相对路径 const resolveRelativePath = (currentPath, relativePath) => { // 获取当前文件所在目录 @@ -1031,12 +1047,33 @@ function DocumentPage() { {(() => { const { fileName, FileIcon, isPdf } = getHeaderDisplay(selectedFile) return ( -
- - - {fileName} - -
+ <> +
+ + + {fileName} + +
+ {viewMode === 'pdf' &&
} + {viewMode === 'markdown' && ( + + + + + )} + ) })()}
@@ -1051,6 +1088,7 @@ function DocumentPage() { ) : (
@@ -1065,72 +1103,17 @@ function DocumentPage() { )}
- {/* 浮动按钮组 - 仅在markdown模式显示 */} - {viewMode === 'markdown' && ( - { - if (!selectedFile) return - let url = getExportPdfUrl(projectId, selectedFile) - const token = localStorage.getItem('access_token') - if (token) { - url += `&token=${encodeURIComponent(token)}` - } - window.open(url, '_blank') - }} - /> - )} - {/* 右侧TOC面板 - 仅在markdown模式显示 */} - {viewMode === 'markdown' && !tocCollapsed && ( - -
-

文档索引

-
-
- {tocItems.length > 0 ? ( - contentRef.current} - items={tocItems.map((item) => ({ - key: item.key, - href: item.href, - title: ( -
- - -
- ), - }))} - /> - ) : ( -
当前文档无标题
- )} -
-
+ {viewMode === 'markdown' && ( + contentRef.current} + renderTitle={(item, keyword) => } + /> )} - - {/* TOC展开按钮 */} - {tocCollapsed && ( - - )} {/* 分享模态框 */} diff --git a/frontend/src/pages/Preview/FileSharePage.jsx b/frontend/src/pages/Preview/FileSharePage.jsx index ed4a9b2..38c8e9d 100644 --- a/frontend/src/pages/Preview/FileSharePage.jsx +++ b/frontend/src/pages/Preview/FileSharePage.jsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef } from 'react' import { useNavigate, useParams } from 'react-router-dom' -import { Layout, Button, Modal, Input, Spin, Anchor } from 'antd' -import { CloseOutlined, LockOutlined, MenuFoldOutlined, MenuUnfoldOutlined, FileTextOutlined, FilePdfOutlined } from '@ant-design/icons' +import { Layout, Modal, Input, Spin, Button, Space } from 'antd' +import { CloseOutlined, LockOutlined, FileTextOutlined, FilePdfOutlined, VerticalAlignTopOutlined, CloudDownloadOutlined } from '@ant-design/icons' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import rehypeHighlight from 'rehype-highlight' @@ -9,7 +9,7 @@ import rehypeSlug from 'rehype-slug' import 'highlight.js/styles/github.css' import GithubSlugger from 'github-slugger' import Toast from '@/components/Toast/Toast' -import DocFloatActions from '@/components/DocFloatActions/DocFloatActions' +import FloatingToc from '@/components/FloatingToc/FloatingToc' import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer' import { getFileSharePublicInfo, @@ -19,17 +19,17 @@ import { } from '@/api/share' import './PreviewPage.css' -const { Content, Sider } = Layout +const { Content } = Layout function FileSharePage() { const { shareCode } = useParams() const navigate = useNavigate() const contentRef = useRef(null) + const [pdfToolbarTarget, setPdfToolbarTarget] = useState(null) const [shareInfo, setShareInfo] = useState(null) const [contentInfo, setContentInfo] = useState(null) const [loading, setLoading] = useState(true) const [isMobile, setIsMobile] = useState(false) - const [tocCollapsed, setTocCollapsed] = useState(false) const [tocItems, setTocItems] = useState([]) const [passwordModalVisible, setPasswordModalVisible] = useState(false) const [password, setPassword] = useState('') @@ -117,6 +117,12 @@ function FileSharePage() { window.open(exportFileSharePDF(shareCode), '_blank') } + const scrollContentToTop = () => { + if (contentRef.current) { + contentRef.current.scrollTo({ top: 0, behavior: 'smooth' }) + } + } + const isExternalHref = (href) => { return Boolean(href && (/^[a-z][a-z\d+.-]*:/i.test(href) || href.startsWith('//'))) } @@ -174,6 +180,25 @@ function FileSharePage() { {headerLabel} + {contentInfo?.type === 'markdown' && ( + + + + + )} + {contentInfo?.type === 'pdf' &&
}
{loading ? (
@@ -184,7 +209,11 @@ function FileSharePage() { ) : (
{contentInfo?.type === 'pdf' ? ( - + ) : (
)} - {contentInfo?.type === 'markdown' && ( - - )} - {!isMobile && contentInfo?.type === 'markdown' && !tocCollapsed && ( - -
-

文档索引

-
-
- {tocItems.length > 0 ? ( - contentRef.current} - items={tocItems.map((item) => ({ - key: item.key, - href: item.href, - title: ( -
- - {item.title} -
- ), - }))} - /> - ) : ( -
当前文档无标题
- )} -
-
+ {!isMobile && contentInfo?.type === 'markdown' && ( + contentRef.current} + /> )} - - {!isMobile && contentInfo?.type === 'markdown' && tocCollapsed && ( - - )}
.ant-anchor-link-title { - color: var(--link-color); - font-weight: 500; -} - -.toc-empty { - color: var(--text-color-secondary); - text-align: center; - margin-top: 40px; - font-size: 13px; -} - -.toc-toggle-btn { - position: fixed; - right: 24px; - top: 50%; - transform: translateY(-50%); - z-index: 100; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); -} - .preview-content { height: 100%; overflow-y: auto; @@ -245,12 +166,15 @@ background: var(--header-bg); display: flex; align-items: center; + justify-content: space-between; + gap: 16px; } .preview-header-title { display: flex; align-items: center; min-width: 0; + flex: 1; margin: 0; font-size: 16px; font-weight: 600; @@ -274,6 +198,20 @@ white-space: nowrap; } +.preview-header-actions { + flex: none; +} + +.pdf-header-toolbar { + min-width: 0; + overflow-x: auto; + overflow-y: hidden; +} + +.pdf-header-toolbar .pdf-toolbar { + white-space: nowrap; +} + .preview-content-header h3:not(.preview-header-title) { margin: 0; font-size: 16px; @@ -494,10 +432,6 @@ width: 240px !important; } - .preview-toc-sider { - width: 200px !important; - } - .preview-content-wrapper { max-width: 100%; padding: 20px; diff --git a/frontend/src/pages/Preview/ProjectSharePage.jsx b/frontend/src/pages/Preview/ProjectSharePage.jsx index c1b8e27..08ced1e 100644 --- a/frontend/src/pages/Preview/ProjectSharePage.jsx +++ b/frontend/src/pages/Preview/ProjectSharePage.jsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, useMemo } from 'react' import { useParams, useSearchParams, useNavigate } from 'react-router-dom' -import { Layout, Menu, Spin, Button, Modal, Input, Drawer, Anchor, Empty, Tooltip } from 'antd' -import { MenuFoldOutlined, MenuUnfoldOutlined, FileTextOutlined, FolderOutlined, FolderOpenOutlined, FilePdfOutlined, LockOutlined, MenuOutlined, CloseOutlined } from '@ant-design/icons' +import { Layout, Menu, Spin, Button, Modal, Input, Drawer, Empty, Tooltip, Space } from 'antd' +import { FileTextOutlined, FolderOutlined, FolderOpenOutlined, FilePdfOutlined, LockOutlined, MenuOutlined, CloseOutlined, VerticalAlignTopOutlined, CloudDownloadOutlined } from '@ant-design/icons' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import rehypeHighlight from 'rehype-highlight' @@ -11,7 +11,7 @@ import Mark from 'mark.js' import Highlighter from 'react-highlight-words' import GithubSlugger from 'github-slugger' import Toast from '@/components/Toast/Toast' -import DocFloatActions from '@/components/DocFloatActions/DocFloatActions' +import FloatingToc from '@/components/FloatingToc/FloatingToc' import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer' import { getProjectSharePublicInfo, @@ -49,7 +49,6 @@ function ProjectSharePage() { const [markdownContent, setMarkdownContent] = useState('') const [loading, setLoading] = useState(false) const [openKeys, setOpenKeys] = useState([]) - const [tocCollapsed, setTocCollapsed] = useState(false) const [tocItems, setTocItems] = useState([]) const [passwordModalVisible, setPasswordModalVisible] = useState(false) const [password, setPassword] = useState('') @@ -64,6 +63,7 @@ function ProjectSharePage() { const [isSearching, setIsSearching] = useState(false) const contentRef = useRef(null) const viewerRef = useRef(null) + const [pdfToolbarTarget, setPdfToolbarTarget] = useState(null) const handleClose = () => { if (window.history.length > 1) { @@ -88,7 +88,7 @@ function ProjectSharePage() { instance.mark(searchKeyword, { element: 'span', className: 'search-highlight', - exclude: ['pre', 'code', '.toc-content'], + exclude: ['pre', 'code', '.floating-toc'], }) } } @@ -381,6 +381,12 @@ function ProjectSharePage() { window.open(exportProjectSharePDF(shareCode, selectedFile), '_blank') } + const scrollContentToTop = () => { + if (contentRef.current) { + contentRef.current.scrollTo({ top: 0, behavior: 'smooth' }) + } + } + const menuItems = useMemo( () => convertTreeToMenuItems(filteredTreeData), [filteredTreeData, openKeys] @@ -510,6 +516,25 @@ function ProjectSharePage() { {headerLabel} + {viewMode === 'markdown' && ( + + + + + )} + {viewMode === 'pdf' &&
}
{loading ? ( @@ -519,7 +544,7 @@ function ProjectSharePage() {
) : viewMode === 'pdf' ? ( - + ) : (
{ if (e.defaultPrevented) return @@ -540,56 +565,17 @@ function ProjectSharePage() { )}
- {viewMode === 'markdown' && ( - - )} - {!isMobile && viewMode === 'markdown' && !tocCollapsed && ( - -
-

文档索引

-
-
- {tocItems.length > 0 ? ( - contentRef.current} - items={tocItems.map((item) => ({ - key: item.key, - href: item.href, - title: ( -
- - -
- ), - }))} - /> - ) : ( -
当前文档无标题
- )} -
-
+ {!isMobile && viewMode === 'markdown' && ( + contentRef.current} + renderTitle={(item, keyword) => } + /> )} - - {!isMobile && tocCollapsed && ( - - )}