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 ? (
+
+
+
+ }
+ onClick={scrollToTop}
+ size="small"
+ type="text"
+ disabled={currentPage === 1}
+ aria-label="回到顶部"
+ />
+
+
+ }
+ onClick={handleDownload}
+ size="small"
+ type="text"
+ aria-label="下载PDF"
+ />
+
+
+
+ ) : (
} onClick={zoomOut} size="small">
diff --git a/frontend/src/pages/Document/DocumentEditor.css b/frontend/src/pages/Document/DocumentEditor.css
index df66b10..6ebef86 100644
--- a/frontend/src/pages/Document/DocumentEditor.css
+++ b/frontend/src/pages/Document/DocumentEditor.css
@@ -283,8 +283,7 @@
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),
+/* Keep the TOC entry visible; hide the help sidebar button. */
.bytemd-toolbar-right .bytemd-toolbar-icon:nth-child(2) {
display: none;
}
diff --git a/frontend/src/pages/Preview/FileSharePage.jsx b/frontend/src/pages/Preview/FileSharePage.jsx
index 38c8e9d..55f4d37 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, Modal, Input, Spin, Button, Space } from 'antd'
-import { CloseOutlined, LockOutlined, FileTextOutlined, FilePdfOutlined, VerticalAlignTopOutlined, CloudDownloadOutlined } from '@ant-design/icons'
+import { Layout, Modal, Input, Spin, Button, Space, Tooltip } from 'antd'
+import { CloseOutlined, LockOutlined, FileTextOutlined, FilePdfOutlined, VerticalAlignTopOutlined, CloudDownloadOutlined, MenuOutlined } 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 FloatingToc from '@/components/FloatingToc/FloatingToc'
+import FloatingToc, { TocDrawer } from '@/components/FloatingToc/FloatingToc'
import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
import {
getFileSharePublicInfo,
@@ -31,6 +31,7 @@ function FileSharePage() {
const [loading, setLoading] = useState(true)
const [isMobile, setIsMobile] = useState(false)
const [tocItems, setTocItems] = useState([])
+ const [tocDrawerVisible, setTocDrawerVisible] = useState(false)
const [passwordModalVisible, setPasswordModalVisible] = useState(false)
const [password, setPassword] = useState('')
@@ -181,22 +182,54 @@ function FileSharePage() {
{headerLabel}
{contentInfo?.type === 'markdown' && (
-
- }
- onClick={scrollContentToTop}
- size="small"
- >
- 回到顶部
-
- }
- onClick={handleExportPDF}
- size="small"
- >
- 下载PDF
-
-
+ isMobile ? (
+
+
+ }
+ onClick={scrollContentToTop}
+ size="small"
+ type="text"
+ aria-label="回到顶部"
+ />
+
+
+ }
+ onClick={handleExportPDF}
+ size="small"
+ type="text"
+ aria-label="下载PDF"
+ />
+
+
+ }
+ onClick={() => setTocDrawerVisible(true)}
+ size="small"
+ type="text"
+ aria-label="文档索引"
+ />
+
+
+ ) : (
+
+ }
+ onClick={scrollContentToTop}
+ size="small"
+ >
+ 回到顶部
+
+ }
+ onClick={handleExportPDF}
+ size="small"
+ >
+ 下载PDF
+
+
+ )
)}
{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 ? (
<>
- }
- className="mobile-close-btn"
- onClick={handleClose}
- />
- }
- className="mobile-menu-btn"
- onClick={() => setMobileDrawerVisible(true)}
- >
- 目录索引
-
openSharedFile(key)}
+ onClick={({ key }) => {
+ openSharedFile(key)
+ setMobileDrawerVisible(false)
+ }}
className="preview-menu"
/>
) : (
@@ -512,27 +502,81 @@ function ProjectSharePage() {
+ {isMobile && (
+
+
+ }
+ onClick={handleClose}
+ size="small"
+ aria-label="关闭项目分享"
+ />
+
+
+ }
+ onClick={() => setMobileDrawerVisible(true)}
+ size="small"
+ aria-label="目录索引"
+ />
+
+
+ )}
{headerLabel}
{viewMode === 'markdown' && (
-
- }
- onClick={scrollContentToTop}
- size="small"
- >
- 回到顶部
-
- }
- onClick={handleExportPDF}
- size="small"
- >
- 下载PDF
-
-
+ isMobile ? (
+
+
+ }
+ onClick={scrollContentToTop}
+ size="small"
+ type="text"
+ aria-label="回到顶部"
+ />
+
+
+ }
+ onClick={handleExportPDF}
+ size="small"
+ type="text"
+ aria-label="下载PDF"
+ />
+
+
+ }
+ onClick={() => setTocDrawerVisible(true)}
+ size="small"
+ type="text"
+ aria-label="文档索引"
+ />
+
+
+ ) : (
+
+ }
+ onClick={scrollContentToTop}
+ size="small"
+ >
+ 回到顶部
+
+ }
+ onClick={handleExportPDF}
+ size="small"
+ >
+ 下载PDF
+
+
+ )
)}
{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}