diff --git a/frontend/src/components/FloatingToc/FloatingToc.css b/frontend/src/components/FloatingToc/FloatingToc.css index 188573f..05b4625 100644 --- a/frontend/src/components/FloatingToc/FloatingToc.css +++ b/frontend/src/components/FloatingToc/FloatingToc.css @@ -74,6 +74,18 @@ transform: translateX(0) scale(1); } +.floating-toc.floating-toc-dismissed .floating-toc-tab { + opacity: 1; + transform: none; + pointer-events: auto; +} + +.floating-toc.floating-toc-dismissed .floating-toc-panel { + opacity: 0; + pointer-events: none; + transform: translateX(10px) scale(0.98); +} + .floating-toc-header { min-height: 48px; padding: 0 16px; @@ -167,6 +179,16 @@ font-size: 13px; } +.floating-toc-drawer .ant-drawer-body { + padding: 0; +} + +.floating-toc-drawer .floating-toc-content { + max-height: none; + height: 100%; + padding: 10px 8px 12px; +} + body.dark .floating-toc-tab, body.dark .floating-toc-panel { box-shadow: 0 22px 48px rgba(0, 0, 0, 0.38); diff --git a/frontend/src/components/FloatingToc/FloatingToc.jsx b/frontend/src/components/FloatingToc/FloatingToc.jsx index ea56046..58dd1b5 100644 --- a/frontend/src/components/FloatingToc/FloatingToc.jsx +++ b/frontend/src/components/FloatingToc/FloatingToc.jsx @@ -1,15 +1,10 @@ -import { Anchor } from 'antd' +import { useState } from 'react' +import { Anchor, Drawer } 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) => ({ +function buildAnchorItems(items, searchKeyword, renderTitle) { + return items.map((item) => ({ key: item.key, href: item.href, title: ( @@ -21,12 +16,74 @@ export default function FloatingToc({ ), })) +} + +function TocContent({ items = [], getContainer, searchKeyword = '', renderTitle, onItemClick }) { + const anchorItems = buildAnchorItems(items, searchKeyword, renderTitle) + + return ( +
+ {items.length > 0 ? ( + { + window.setTimeout(() => onItemClick?.(link), 120) + }} + /> + ) : ( +
当前文档无标题
+ )} +
+ ) +} + +export function TocDrawer({ + open, + onClose, + items = [], + getContainer, + searchKeyword = '', + renderTitle, +}) { + return ( + + + + ) +} + +export default function FloatingToc({ + items = [], + getContainer, + searchKeyword = '', + renderTitle, + className = '', +}) { + const [dismissed, setDismissed] = useState(false) return ( ) diff --git a/frontend/src/components/PDFViewer/VirtualPDFViewer.css b/frontend/src/components/PDFViewer/VirtualPDFViewer.css index 7230ccd..61fec14 100644 --- a/frontend/src/components/PDFViewer/VirtualPDFViewer.css +++ b/frontend/src/components/PDFViewer/VirtualPDFViewer.css @@ -30,6 +30,20 @@ display: inline-block; } +.pdf-toolbar-compact .ant-btn { + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text-color-secondary); +} + +.pdf-toolbar-compact .ant-btn:hover { + background: var(--item-hover-bg); + color: var(--link-color); +} + .pdf-content { flex: 1; overflow: auto; diff --git a/frontend/src/components/PDFViewer/VirtualPDFViewer.jsx b/frontend/src/components/PDFViewer/VirtualPDFViewer.jsx index 60b1558..9330494 100644 --- a/frontend/src/components/PDFViewer/VirtualPDFViewer.jsx +++ b/frontend/src/components/PDFViewer/VirtualPDFViewer.jsx @@ -1,7 +1,7 @@ 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 { Button, Space, InputNumber, message, Spin, Tooltip } from 'antd' import { ZoomInOutlined, ZoomOutOutlined, @@ -17,7 +17,7 @@ import './VirtualPDFViewer.css' // 配置 PDF.js worker pdfjs.GlobalWorkerOptions.workerSrc = '/pdf-worker/pdf.worker.min.mjs' -function VirtualPDFViewer({ url, filename, toolbarTarget }) { +function VirtualPDFViewer({ url, filename, toolbarTarget, compactToolbar = false }) { const [numPages, setNumPages] = useState(null) const [scale, setScale] = useState(1.0) const [pdfOriginalSize, setPdfOriginalSize] = useState({ width: 595, height: 842 }) // 默认 A4 @@ -160,7 +160,31 @@ function VirtualPDFViewer({ url, filename, toolbarTarget }) { document.body.removeChild(link) } - const toolbar = ( + const toolbar = compactToolbar ? ( +
+ + +
+ ) : (
- - + isMobile ? ( + + + + + + ) )} {contentInfo?.type === 'pdf' &&
}
@@ -213,6 +246,7 @@ function FileSharePage() { url={contentInfo.document_url} filename={contentInfo.filename} toolbarTarget={pdfToolbarTarget} + compactToolbar={isMobile} /> ) : (
@@ -239,6 +273,13 @@ function FileSharePage() {
+ setTocDrawerVisible(false)} + items={tocItems} + getContainer={() => contentRef.current} + /> + 访问验证
} open={passwordModalVisible} diff --git a/frontend/src/pages/Preview/PreviewPage.css b/frontend/src/pages/Preview/PreviewPage.css index 165b717..4c37db0 100644 --- a/frontend/src/pages/Preview/PreviewPage.css +++ b/frontend/src/pages/Preview/PreviewPage.css @@ -170,6 +170,29 @@ gap: 16px; } +.preview-header-leading-actions { + display: inline-flex; + align-items: center; + gap: 4px; + flex: none; +} + +.preview-header-leading-actions .ant-btn, +.preview-compact-actions .ant-btn { + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text-color-secondary); +} + +.preview-header-leading-actions .ant-btn:hover, +.preview-compact-actions .ant-btn:hover { + background: var(--item-hover-bg); + color: var(--link-color); +} + .preview-header-title { display: flex; align-items: center; @@ -365,31 +388,23 @@ margin-bottom: 4px; } -/* 移动端菜单按钮 */ -.mobile-menu-btn { - position: fixed; - top: 16px; - left: 16px; - z-index: 1000; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); -} - -.mobile-close-btn { - position: fixed; - top: 16px; - right: 16px; - z-index: 1000; - background: var(--header-bg); - border: 1px solid var(--border-color); - border-radius: 999px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); -} - /* 移动端响应式样式 */ @media (max-width: 768px) { + .preview-content-header { + padding: 12px 12px; + gap: 8px; + } + + .file-share-content-header { + gap: 8px; + } + .preview-content-wrapper { padding: 16px; - padding-top: 60px; /* 为移动端菜单按钮留出空间 */ + } + + .preview-content-wrapper.pdf-mode { + padding: 0; } .markdown-body { @@ -444,6 +459,10 @@ padding: 12px; } + .preview-content-wrapper.pdf-mode { + padding: 0; + } + .markdown-body { font-size: 14px; } diff --git a/frontend/src/pages/Preview/ProjectSharePage.jsx b/frontend/src/pages/Preview/ProjectSharePage.jsx index 08ced1e..8f9d0d4 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, Empty, Tooltip, Space } from 'antd' -import { FileTextOutlined, FolderOutlined, FolderOpenOutlined, FilePdfOutlined, LockOutlined, MenuOutlined, CloseOutlined, VerticalAlignTopOutlined, CloudDownloadOutlined } from '@ant-design/icons' +import { FileTextOutlined, FolderOutlined, FolderOpenOutlined, FilePdfOutlined, LockOutlined, MenuOutlined, CloseOutlined, VerticalAlignTopOutlined, CloudDownloadOutlined, UnorderedListOutlined } 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 FloatingToc from '@/components/FloatingToc/FloatingToc' +import FloatingToc, { TocDrawer } from '@/components/FloatingToc/FloatingToc' import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer' import { getProjectSharePublicInfo, @@ -54,6 +54,7 @@ function ProjectSharePage() { const [password, setPassword] = useState('') const [siderCollapsed, setSiderCollapsed] = useState(false) const [mobileDrawerVisible, setMobileDrawerVisible] = useState(false) + const [tocDrawerVisible, setTocDrawerVisible] = useState(false) const [isMobile, setIsMobile] = useState(false) const [pdfUrl, setPdfUrl] = useState('') const [pdfFilename, setPdfFilename] = useState('') @@ -418,20 +419,6 @@ function ProjectSharePage() { {isMobile ? ( <> - openSharedFile(key)} + onClick={({ key }) => { + openSharedFile(key) + setMobileDrawerVisible(false) + }} className="preview-menu" /> ) : ( @@ -512,27 +502,81 @@ function ProjectSharePage() {
+ {isMobile && ( +
+ +
+ )}

{headerLabel}

{viewMode === 'markdown' && ( - - - - + isMobile ? ( + + + + + + ) )} {viewMode === 'pdf' &&
}
@@ -544,7 +588,7 @@ function ProjectSharePage() {
) : viewMode === 'pdf' ? ( - + ) : (
{ if (e.defaultPrevented) return @@ -578,6 +622,15 @@ function ProjectSharePage() { + setTocDrawerVisible(false)} + items={tocItems} + searchKeyword={searchKeyword} + getContainer={() => contentRef.current} + renderTitle={(item, keyword) => } + /> + 访问验证
} open={passwordModalVisible}