v0.9.7
parent
4b41763ef4
commit
fd10178367
|
|
@ -74,6 +74,18 @@
|
||||||
transform: translateX(0) scale(1);
|
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 {
|
.floating-toc-header {
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
|
|
@ -167,6 +179,16 @@
|
||||||
font-size: 13px;
|
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-tab,
|
||||||
body.dark .floating-toc-panel {
|
body.dark .floating-toc-panel {
|
||||||
box-shadow: 0 22px 48px rgba(0, 0, 0, 0.38);
|
box-shadow: 0 22px 48px rgba(0, 0, 0, 0.38);
|
||||||
|
|
|
||||||
|
|
@ -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 { FileTextOutlined, MenuOutlined } from '@ant-design/icons'
|
||||||
import './FloatingToc.css'
|
import './FloatingToc.css'
|
||||||
|
|
||||||
export default function FloatingToc({
|
function buildAnchorItems(items, searchKeyword, renderTitle) {
|
||||||
items = [],
|
return items.map((item) => ({
|
||||||
getContainer,
|
|
||||||
searchKeyword = '',
|
|
||||||
renderTitle,
|
|
||||||
className = '',
|
|
||||||
}) {
|
|
||||||
const anchorItems = items.map((item) => ({
|
|
||||||
key: item.key,
|
key: item.key,
|
||||||
href: item.href,
|
href: item.href,
|
||||||
title: (
|
title: (
|
||||||
|
|
@ -21,12 +16,74 @@ export default function FloatingToc({
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function TocContent({ items = [], getContainer, searchKeyword = '', renderTitle, onItemClick }) {
|
||||||
|
const anchorItems = buildAnchorItems(items, searchKeyword, renderTitle)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="floating-toc-content">
|
||||||
|
{items.length > 0 ? (
|
||||||
|
<Anchor
|
||||||
|
affix={false}
|
||||||
|
offsetTop={0}
|
||||||
|
getContainer={getContainer}
|
||||||
|
items={anchorItems}
|
||||||
|
onClick={(e, link) => {
|
||||||
|
window.setTimeout(() => onItemClick?.(link), 120)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="floating-toc-empty">当前文档无标题</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TocDrawer({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
items = [],
|
||||||
|
getContainer,
|
||||||
|
searchKeyword = '',
|
||||||
|
renderTitle,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
title="文档索引"
|
||||||
|
placement="right"
|
||||||
|
onClose={onClose}
|
||||||
|
open={open}
|
||||||
|
width="82%"
|
||||||
|
className="floating-toc-drawer"
|
||||||
|
>
|
||||||
|
<TocContent
|
||||||
|
items={items}
|
||||||
|
getContainer={getContainer}
|
||||||
|
searchKeyword={searchKeyword}
|
||||||
|
renderTitle={renderTitle}
|
||||||
|
onItemClick={onClose}
|
||||||
|
/>
|
||||||
|
</Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FloatingToc({
|
||||||
|
items = [],
|
||||||
|
getContainer,
|
||||||
|
searchKeyword = '',
|
||||||
|
renderTitle,
|
||||||
|
className = '',
|
||||||
|
}) {
|
||||||
|
const [dismissed, setDismissed] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
className={`floating-toc ${className}`.trim()}
|
className={`floating-toc ${dismissed ? 'floating-toc-dismissed' : ''} ${className}`.trim()}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label="文档索引"
|
aria-label="文档索引"
|
||||||
|
onMouseEnter={() => setDismissed(false)}
|
||||||
|
onFocus={() => setDismissed(false)}
|
||||||
>
|
>
|
||||||
<div className="floating-toc-tab">
|
<div className="floating-toc-tab">
|
||||||
<MenuOutlined />
|
<MenuOutlined />
|
||||||
|
|
@ -37,18 +94,13 @@ export default function FloatingToc({
|
||||||
<span>文档索引</span>
|
<span>文档索引</span>
|
||||||
{items.length > 0 && <span className="floating-toc-count">{items.length}</span>}
|
{items.length > 0 && <span className="floating-toc-count">{items.length}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="floating-toc-content">
|
<TocContent
|
||||||
{items.length > 0 ? (
|
items={items}
|
||||||
<Anchor
|
getContainer={getContainer}
|
||||||
affix={false}
|
searchKeyword={searchKeyword}
|
||||||
offsetTop={0}
|
renderTitle={renderTitle}
|
||||||
getContainer={getContainer}
|
onItemClick={() => setDismissed(true)}
|
||||||
items={anchorItems}
|
/>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="floating-toc-empty">当前文档无标题</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,20 @@
|
||||||
display: inline-block;
|
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 {
|
.pdf-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState, useMemo, useRef, useEffect, useCallback } from 'react'
|
import { useState, useMemo, useRef, useEffect, useCallback } from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import { Document, Page, pdfjs } from 'react-pdf'
|
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 {
|
import {
|
||||||
ZoomInOutlined,
|
ZoomInOutlined,
|
||||||
ZoomOutOutlined,
|
ZoomOutOutlined,
|
||||||
|
|
@ -17,7 +17,7 @@ import './VirtualPDFViewer.css'
|
||||||
// 配置 PDF.js worker
|
// 配置 PDF.js worker
|
||||||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf-worker/pdf.worker.min.mjs'
|
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 [numPages, setNumPages] = useState(null)
|
||||||
const [scale, setScale] = useState(1.0)
|
const [scale, setScale] = useState(1.0)
|
||||||
const [pdfOriginalSize, setPdfOriginalSize] = useState({ width: 595, height: 842 }) // 默认 A4
|
const [pdfOriginalSize, setPdfOriginalSize] = useState({ width: 595, height: 842 }) // 默认 A4
|
||||||
|
|
@ -160,7 +160,31 @@ function VirtualPDFViewer({ url, filename, toolbarTarget }) {
|
||||||
document.body.removeChild(link)
|
document.body.removeChild(link)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolbar = (
|
const toolbar = compactToolbar ? (
|
||||||
|
<div className="pdf-toolbar pdf-toolbar-compact">
|
||||||
|
<Space size={4}>
|
||||||
|
<Tooltip title="回到顶部">
|
||||||
|
<Button
|
||||||
|
icon={<VerticalAlignTopOutlined />}
|
||||||
|
onClick={scrollToTop}
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
aria-label="回到顶部"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="下载PDF">
|
||||||
|
<Button
|
||||||
|
icon={<CloudDownloadOutlined />}
|
||||||
|
onClick={handleDownload}
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
aria-label="下载PDF"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="pdf-toolbar">
|
<div className="pdf-toolbar">
|
||||||
<Space>
|
<Space>
|
||||||
<Button icon={<ZoomOutOutlined />} onClick={zoomOut} size="small">
|
<Button icon={<ZoomOutOutlined />} onClick={zoomOut} size="small">
|
||||||
|
|
|
||||||
|
|
@ -283,8 +283,7 @@
|
||||||
background-color: var(--item-hover-bg);
|
background-color: var(--item-hover-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The fixed editor layout does not use Bytemd's built-in sidebar modes. */
|
/* Keep the TOC entry visible; hide the help sidebar button. */
|
||||||
.bytemd-toolbar-right .bytemd-toolbar-icon:nth-child(1),
|
|
||||||
.bytemd-toolbar-right .bytemd-toolbar-icon:nth-child(2) {
|
.bytemd-toolbar-right .bytemd-toolbar-icon:nth-child(2) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import { Layout, Modal, Input, Spin, Button, Space } from 'antd'
|
import { Layout, Modal, Input, Spin, Button, Space, Tooltip } from 'antd'
|
||||||
import { CloseOutlined, LockOutlined, FileTextOutlined, FilePdfOutlined, VerticalAlignTopOutlined, CloudDownloadOutlined } from '@ant-design/icons'
|
import { CloseOutlined, LockOutlined, FileTextOutlined, FilePdfOutlined, VerticalAlignTopOutlined, CloudDownloadOutlined, MenuOutlined } from '@ant-design/icons'
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from 'react-markdown'
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
import rehypeHighlight from 'rehype-highlight'
|
import rehypeHighlight from 'rehype-highlight'
|
||||||
|
|
@ -9,7 +9,7 @@ import rehypeSlug from 'rehype-slug'
|
||||||
import 'highlight.js/styles/github.css'
|
import 'highlight.js/styles/github.css'
|
||||||
import GithubSlugger from 'github-slugger'
|
import GithubSlugger from 'github-slugger'
|
||||||
import Toast from '@/components/Toast/Toast'
|
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 VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
||||||
import {
|
import {
|
||||||
getFileSharePublicInfo,
|
getFileSharePublicInfo,
|
||||||
|
|
@ -31,6 +31,7 @@ function FileSharePage() {
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [isMobile, setIsMobile] = useState(false)
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
const [tocItems, setTocItems] = useState([])
|
const [tocItems, setTocItems] = useState([])
|
||||||
|
const [tocDrawerVisible, setTocDrawerVisible] = useState(false)
|
||||||
const [passwordModalVisible, setPasswordModalVisible] = useState(false)
|
const [passwordModalVisible, setPasswordModalVisible] = useState(false)
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
|
|
||||||
|
|
@ -181,22 +182,54 @@ function FileSharePage() {
|
||||||
<span className="preview-header-text">{headerLabel}</span>
|
<span className="preview-header-text">{headerLabel}</span>
|
||||||
</h3>
|
</h3>
|
||||||
{contentInfo?.type === 'markdown' && (
|
{contentInfo?.type === 'markdown' && (
|
||||||
<Space className="preview-header-actions">
|
isMobile ? (
|
||||||
<Button
|
<Space className="preview-header-actions preview-compact-actions" size={4}>
|
||||||
icon={<VerticalAlignTopOutlined />}
|
<Tooltip title="回到顶部">
|
||||||
onClick={scrollContentToTop}
|
<Button
|
||||||
size="small"
|
icon={<VerticalAlignTopOutlined />}
|
||||||
>
|
onClick={scrollContentToTop}
|
||||||
回到顶部
|
size="small"
|
||||||
</Button>
|
type="text"
|
||||||
<Button
|
aria-label="回到顶部"
|
||||||
icon={<CloudDownloadOutlined />}
|
/>
|
||||||
onClick={handleExportPDF}
|
</Tooltip>
|
||||||
size="small"
|
<Tooltip title="下载PDF">
|
||||||
>
|
<Button
|
||||||
下载PDF
|
icon={<CloudDownloadOutlined />}
|
||||||
</Button>
|
onClick={handleExportPDF}
|
||||||
</Space>
|
size="small"
|
||||||
|
type="text"
|
||||||
|
aria-label="下载PDF"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="文档索引">
|
||||||
|
<Button
|
||||||
|
icon={<MenuOutlined />}
|
||||||
|
onClick={() => setTocDrawerVisible(true)}
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
aria-label="文档索引"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<Space className="preview-header-actions">
|
||||||
|
<Button
|
||||||
|
icon={<VerticalAlignTopOutlined />}
|
||||||
|
onClick={scrollContentToTop}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
回到顶部
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<CloudDownloadOutlined />}
|
||||||
|
onClick={handleExportPDF}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
下载PDF
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
{contentInfo?.type === 'pdf' && <div className="preview-header-actions pdf-header-toolbar" ref={setPdfToolbarTarget} />}
|
{contentInfo?.type === 'pdf' && <div className="preview-header-actions pdf-header-toolbar" ref={setPdfToolbarTarget} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -213,6 +246,7 @@ function FileSharePage() {
|
||||||
url={contentInfo.document_url}
|
url={contentInfo.document_url}
|
||||||
filename={contentInfo.filename}
|
filename={contentInfo.filename}
|
||||||
toolbarTarget={pdfToolbarTarget}
|
toolbarTarget={pdfToolbarTarget}
|
||||||
|
compactToolbar={isMobile}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="markdown-body">
|
<div className="markdown-body">
|
||||||
|
|
@ -239,6 +273,13 @@ function FileSharePage() {
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TocDrawer
|
||||||
|
open={tocDrawerVisible}
|
||||||
|
onClose={() => setTocDrawerVisible(false)}
|
||||||
|
items={tocItems}
|
||||||
|
getContainer={() => contentRef.current}
|
||||||
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><LockOutlined /><span>访问验证</span></div>}
|
title={<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><LockOutlined /><span>访问验证</span></div>}
|
||||||
open={passwordModalVisible}
|
open={passwordModalVisible}
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,29 @@
|
||||||
gap: 16px;
|
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 {
|
.preview-header-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -365,31 +388,23 @@
|
||||||
margin-bottom: 4px;
|
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) {
|
@media (max-width: 768px) {
|
||||||
|
.preview-content-header {
|
||||||
|
padding: 12px 12px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-share-content-header {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.preview-content-wrapper {
|
.preview-content-wrapper {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
padding-top: 60px; /* 为移动端菜单按钮留出空间 */
|
}
|
||||||
|
|
||||||
|
.preview-content-wrapper.pdf-mode {
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body {
|
.markdown-body {
|
||||||
|
|
@ -444,6 +459,10 @@
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-content-wrapper.pdf-mode {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.markdown-body {
|
.markdown-body {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState, useEffect, useRef, useMemo } from 'react'
|
import { useState, useEffect, useRef, useMemo } from 'react'
|
||||||
import { useParams, useSearchParams, useNavigate } from 'react-router-dom'
|
import { useParams, useSearchParams, useNavigate } from 'react-router-dom'
|
||||||
import { Layout, Menu, Spin, Button, Modal, Input, Drawer, Empty, Tooltip, Space } from 'antd'
|
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 ReactMarkdown from 'react-markdown'
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
import rehypeHighlight from 'rehype-highlight'
|
import rehypeHighlight from 'rehype-highlight'
|
||||||
|
|
@ -11,7 +11,7 @@ import Mark from 'mark.js'
|
||||||
import Highlighter from 'react-highlight-words'
|
import Highlighter from 'react-highlight-words'
|
||||||
import GithubSlugger from 'github-slugger'
|
import GithubSlugger from 'github-slugger'
|
||||||
import Toast from '@/components/Toast/Toast'
|
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 VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
||||||
import {
|
import {
|
||||||
getProjectSharePublicInfo,
|
getProjectSharePublicInfo,
|
||||||
|
|
@ -54,6 +54,7 @@ function ProjectSharePage() {
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [siderCollapsed, setSiderCollapsed] = useState(false)
|
const [siderCollapsed, setSiderCollapsed] = useState(false)
|
||||||
const [mobileDrawerVisible, setMobileDrawerVisible] = useState(false)
|
const [mobileDrawerVisible, setMobileDrawerVisible] = useState(false)
|
||||||
|
const [tocDrawerVisible, setTocDrawerVisible] = useState(false)
|
||||||
const [isMobile, setIsMobile] = useState(false)
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
const [pdfUrl, setPdfUrl] = useState('')
|
const [pdfUrl, setPdfUrl] = useState('')
|
||||||
const [pdfFilename, setPdfFilename] = useState('')
|
const [pdfFilename, setPdfFilename] = useState('')
|
||||||
|
|
@ -418,20 +419,6 @@ function ProjectSharePage() {
|
||||||
<Layout className="preview-layout">
|
<Layout className="preview-layout">
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<CloseOutlined />}
|
|
||||||
className="mobile-close-btn"
|
|
||||||
onClick={handleClose}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<MenuOutlined />}
|
|
||||||
className="mobile-menu-btn"
|
|
||||||
onClick={() => setMobileDrawerVisible(true)}
|
|
||||||
>
|
|
||||||
目录索引
|
|
||||||
</Button>
|
|
||||||
<Drawer
|
<Drawer
|
||||||
title={projectInfo?.name || '项目分享'}
|
title={projectInfo?.name || '项目分享'}
|
||||||
placement="left"
|
placement="left"
|
||||||
|
|
@ -457,7 +444,10 @@ function ProjectSharePage() {
|
||||||
openKeys={openKeys}
|
openKeys={openKeys}
|
||||||
onOpenChange={setOpenKeys}
|
onOpenChange={setOpenKeys}
|
||||||
items={menuItems}
|
items={menuItems}
|
||||||
onClick={({ key }) => openSharedFile(key)}
|
onClick={({ key }) => {
|
||||||
|
openSharedFile(key)
|
||||||
|
setMobileDrawerVisible(false)
|
||||||
|
}}
|
||||||
className="preview-menu"
|
className="preview-menu"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -512,27 +502,81 @@ function ProjectSharePage() {
|
||||||
<Layout className="preview-content-layout">
|
<Layout className="preview-content-layout">
|
||||||
<Content className="preview-content" ref={contentRef}>
|
<Content className="preview-content" ref={contentRef}>
|
||||||
<div className="preview-content-header">
|
<div className="preview-content-header">
|
||||||
|
{isMobile && (
|
||||||
|
<div className="preview-header-leading-actions">
|
||||||
|
<Tooltip title="关闭">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
onClick={handleClose}
|
||||||
|
size="small"
|
||||||
|
aria-label="关闭项目分享"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="目录索引">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<UnorderedListOutlined />}
|
||||||
|
onClick={() => setMobileDrawerVisible(true)}
|
||||||
|
size="small"
|
||||||
|
aria-label="目录索引"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<h3 className="preview-header-title">
|
<h3 className="preview-header-title">
|
||||||
<HeaderIcon className="preview-header-icon" style={isHeaderPdf ? { color: '#f5222d' } : undefined} />
|
<HeaderIcon className="preview-header-icon" style={isHeaderPdf ? { color: '#f5222d' } : undefined} />
|
||||||
<span className="preview-header-text">{headerLabel}</span>
|
<span className="preview-header-text">{headerLabel}</span>
|
||||||
</h3>
|
</h3>
|
||||||
{viewMode === 'markdown' && (
|
{viewMode === 'markdown' && (
|
||||||
<Space className="preview-header-actions">
|
isMobile ? (
|
||||||
<Button
|
<Space className="preview-header-actions preview-compact-actions" size={4}>
|
||||||
icon={<VerticalAlignTopOutlined />}
|
<Tooltip title="回到顶部">
|
||||||
onClick={scrollContentToTop}
|
<Button
|
||||||
size="small"
|
icon={<VerticalAlignTopOutlined />}
|
||||||
>
|
onClick={scrollContentToTop}
|
||||||
回到顶部
|
size="small"
|
||||||
</Button>
|
type="text"
|
||||||
<Button
|
aria-label="回到顶部"
|
||||||
icon={<CloudDownloadOutlined />}
|
/>
|
||||||
onClick={handleExportPDF}
|
</Tooltip>
|
||||||
size="small"
|
<Tooltip title="下载PDF">
|
||||||
>
|
<Button
|
||||||
下载PDF
|
icon={<CloudDownloadOutlined />}
|
||||||
</Button>
|
onClick={handleExportPDF}
|
||||||
</Space>
|
size="small"
|
||||||
|
type="text"
|
||||||
|
aria-label="下载PDF"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="文档索引">
|
||||||
|
<Button
|
||||||
|
icon={<MenuOutlined />}
|
||||||
|
onClick={() => setTocDrawerVisible(true)}
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
aria-label="文档索引"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<Space className="preview-header-actions">
|
||||||
|
<Button
|
||||||
|
icon={<VerticalAlignTopOutlined />}
|
||||||
|
onClick={scrollContentToTop}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
回到顶部
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<CloudDownloadOutlined />}
|
||||||
|
onClick={handleExportPDF}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
下载PDF
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
{viewMode === 'pdf' && <div className="preview-header-actions pdf-header-toolbar" ref={setPdfToolbarTarget} />}
|
{viewMode === 'pdf' && <div className="preview-header-actions pdf-header-toolbar" ref={setPdfToolbarTarget} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -544,7 +588,7 @@ function ProjectSharePage() {
|
||||||
</Spin>
|
</Spin>
|
||||||
</div>
|
</div>
|
||||||
) : viewMode === 'pdf' ? (
|
) : viewMode === 'pdf' ? (
|
||||||
<VirtualPDFViewer url={pdfUrl} filename={pdfFilename} toolbarTarget={pdfToolbarTarget} />
|
<VirtualPDFViewer url={pdfUrl} filename={pdfFilename} toolbarTarget={pdfToolbarTarget} compactToolbar={isMobile} />
|
||||||
) : (
|
) : (
|
||||||
<div className="markdown-body" onClick={(e) => {
|
<div className="markdown-body" onClick={(e) => {
|
||||||
if (e.defaultPrevented) return
|
if (e.defaultPrevented) return
|
||||||
|
|
@ -578,6 +622,15 @@ function ProjectSharePage() {
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
<TocDrawer
|
||||||
|
open={tocDrawerVisible}
|
||||||
|
onClose={() => setTocDrawerVisible(false)}
|
||||||
|
items={tocItems}
|
||||||
|
searchKeyword={searchKeyword}
|
||||||
|
getContainer={() => contentRef.current}
|
||||||
|
renderTitle={(item, keyword) => <HighlightText text={item.title} keyword={keyword} />}
|
||||||
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><LockOutlined /><span>访问验证</span></div>}
|
title={<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><LockOutlined /><span>访问验证</span></div>}
|
||||||
open={passwordModalVisible}
|
open={passwordModalVisible}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue