调整了多个页面的工具栏样式
parent
5256d20ac9
commit
9f395a10ac
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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: (
|
||||||
|
<div className="floating-toc-item" style={{ paddingLeft: `${(item.level - 1) * 12}px` }}>
|
||||||
|
<FileTextOutlined className="floating-toc-item-icon" />
|
||||||
|
<span className="floating-toc-item-title">
|
||||||
|
{renderTitle ? renderTitle(item, searchKeyword) : item.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside
|
||||||
|
className={`floating-toc ${className}`.trim()}
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label="文档索引"
|
||||||
|
>
|
||||||
|
<div className="floating-toc-tab">
|
||||||
|
<MenuOutlined />
|
||||||
|
<span>文档索引</span>
|
||||||
|
</div>
|
||||||
|
<div className="floating-toc-panel">
|
||||||
|
<div className="floating-toc-header">
|
||||||
|
<span>文档索引</span>
|
||||||
|
{items.length > 0 && <span className="floating-toc-count">{items.length}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="floating-toc-content">
|
||||||
|
{items.length > 0 ? (
|
||||||
|
<Anchor
|
||||||
|
affix={false}
|
||||||
|
offsetTop={0}
|
||||||
|
getContainer={getContainer}
|
||||||
|
items={anchorItems}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="floating-toc-empty">当前文档无标题</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -7,22 +7,42 @@
|
||||||
|
|
||||||
.pdf-toolbar {
|
.pdf-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px 16px;
|
gap: 16px;
|
||||||
background: var(--card-bg);
|
min-width: 0;
|
||||||
border-bottom: 1px solid var(--border-color);
|
background: transparent;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
color: var(--text-color);
|
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 {
|
.pdf-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
position: relative;
|
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 {
|
.pdf-virtual-list {
|
||||||
background: var(--bg-color-secondary);
|
background: var(--bg-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useState, useMemo, useRef, useEffect, useCallback } from 'react'
|
import { useState, useMemo, useRef, useEffect, useCallback } from 'react'
|
||||||
|
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 } from 'antd'
|
||||||
import {
|
import {
|
||||||
|
|
@ -16,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 }) {
|
function VirtualPDFViewer({ url, filename, toolbarTarget }) {
|
||||||
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
|
||||||
|
|
@ -159,65 +160,66 @@ function VirtualPDFViewer({ url, filename }) {
|
||||||
document.body.removeChild(link)
|
document.body.removeChild(link)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toolbar = (
|
||||||
|
<div className="pdf-toolbar">
|
||||||
|
<Space>
|
||||||
|
<Button icon={<ZoomOutOutlined />} onClick={zoomOut} size="small">
|
||||||
|
缩小
|
||||||
|
</Button>
|
||||||
|
<span className="pdf-toolbar-scale">
|
||||||
|
{Math.round(scale * 100)}%
|
||||||
|
</span>
|
||||||
|
<Button icon={<ZoomInOutlined />} onClick={zoomIn} size="small">
|
||||||
|
放大
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<LeftOutlined />}
|
||||||
|
onClick={() => handlePageChange(currentPage - 1)}
|
||||||
|
disabled={currentPage <= 1}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<Space.Compact>
|
||||||
|
<InputNumber
|
||||||
|
min={1}
|
||||||
|
max={numPages || 1}
|
||||||
|
value={currentPage}
|
||||||
|
onChange={handlePageChange}
|
||||||
|
size="small"
|
||||||
|
style={{ width: 60 }}
|
||||||
|
/>
|
||||||
|
<Button size="small" disabled>
|
||||||
|
/ {numPages || 0}
|
||||||
|
</Button>
|
||||||
|
</Space.Compact>
|
||||||
|
<Button
|
||||||
|
icon={<RightOutlined />}
|
||||||
|
onClick={() => handlePageChange(currentPage + 1)}
|
||||||
|
disabled={currentPage >= (numPages || 0)}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<span className="pdf-toolbar-divider" />
|
||||||
|
<Button
|
||||||
|
icon={<VerticalAlignTopOutlined />}
|
||||||
|
onClick={scrollToTop}
|
||||||
|
size="small"
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
回到顶部
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<CloudDownloadOutlined />}
|
||||||
|
onClick={handleDownload}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
下载PDF
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="virtual-pdf-viewer-container">
|
<div className="virtual-pdf-viewer-container">
|
||||||
{/* 工具栏 */}
|
{toolbarTarget ? createPortal(toolbar, toolbarTarget) : toolbar}
|
||||||
<div className="pdf-toolbar">
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
icon={<LeftOutlined />}
|
|
||||||
onClick={() => handlePageChange(currentPage - 1)}
|
|
||||||
disabled={currentPage <= 1}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
<Space.Compact>
|
|
||||||
<InputNumber
|
|
||||||
min={1}
|
|
||||||
max={numPages || 1}
|
|
||||||
value={currentPage}
|
|
||||||
onChange={handlePageChange}
|
|
||||||
size="small"
|
|
||||||
style={{ width: 60 }}
|
|
||||||
/>
|
|
||||||
<Button size="small" disabled>
|
|
||||||
/ {numPages || 0}
|
|
||||||
</Button>
|
|
||||||
</Space.Compact>
|
|
||||||
<Button
|
|
||||||
icon={<RightOutlined />}
|
|
||||||
onClick={() => handlePageChange(currentPage + 1)}
|
|
||||||
disabled={currentPage >= (numPages || 0)}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
icon={<VerticalAlignTopOutlined />}
|
|
||||||
onClick={scrollToTop}
|
|
||||||
size="small"
|
|
||||||
disabled={currentPage === 1}
|
|
||||||
>
|
|
||||||
回到顶部
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
icon={<CloudDownloadOutlined />}
|
|
||||||
onClick={handleDownload}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
下载PDF
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
|
|
||||||
<Space>
|
|
||||||
<Button icon={<ZoomOutOutlined />} onClick={zoomOut} size="small">
|
|
||||||
缩小
|
|
||||||
</Button>
|
|
||||||
<span style={{ minWidth: 50, textAlign: 'center' }}>
|
|
||||||
{Math.round(scale * 100)}%
|
|
||||||
</span>
|
|
||||||
<Button icon={<ZoomInOutlined />} onClick={zoomIn} size="small">
|
|
||||||
放大
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* PDF内容区 - 自定义虚拟滚动 */}
|
{/* PDF内容区 - 自定义虚拟滚动 */}
|
||||||
<div className="pdf-content" ref={containerRef}>
|
<div className="pdf-content" ref={containerRef}>
|
||||||
|
|
|
||||||
|
|
@ -283,6 +283,12 @@
|
||||||
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. */
|
||||||
|
.bytemd-toolbar-right .bytemd-toolbar-icon:nth-child(1),
|
||||||
|
.bytemd-toolbar-right .bytemd-toolbar-icon:nth-child(2) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* 编辑和预览区域容器 */
|
/* 编辑和预览区域容器 */
|
||||||
.bytemd-body {
|
.bytemd-body {
|
||||||
flex: 1 !important;
|
flex: 1 !important;
|
||||||
|
|
@ -297,15 +303,10 @@
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 编辑区域 - 固定50%宽度 */
|
/* 编辑区域 - 默认分栏,保留 Bytemd 内联样式对仅编辑/仅预览的控制 */
|
||||||
.bytemd-editor {
|
.bytemd-editor {
|
||||||
width: 50% !important;
|
|
||||||
flex: 0 0 50% !important;
|
|
||||||
display: flex !important;
|
|
||||||
flex-direction: column !important;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
max-width: 50% !important;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
/* Added for consistent box model */
|
/* Added for consistent box model */
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
@ -314,16 +315,13 @@
|
||||||
border-right: 1px solid var(--border-color);
|
border-right: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 预览区域 - 固定50%宽度 */
|
/* 预览区域 - 默认分栏,保留 Bytemd 内联样式对仅编辑/仅预览的控制 */
|
||||||
.bytemd-preview {
|
.bytemd-preview {
|
||||||
width: 50% !important;
|
|
||||||
flex: 0 0 50% !important;
|
|
||||||
overflow-y: auto !important;
|
overflow-y: auto !important;
|
||||||
overflow-x: hidden !important;
|
overflow-x: hidden !important;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
max-width: 50% !important;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
/* Added for consistent box model */
|
/* Added for consistent box model */
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
|
||||||
|
|
@ -169,86 +169,6 @@
|
||||||
background: var(--bg-color);
|
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 {
|
.docs-content {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
@ -265,13 +185,15 @@
|
||||||
background: var(--header-bg);
|
background: var(--header-bg);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-header-title {
|
.docs-header-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
max-width: 100%;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
|
|
@ -301,6 +223,20 @@
|
||||||
white-space: nowrap;
|
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 {
|
.docs-content-wrapper {
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState, useEffect, useRef, useMemo } from 'react'
|
import { useState, useEffect, useRef, useMemo } from 'react'
|
||||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
|
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 { Layout, Menu, Spin, Button, Tooltip, message, 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 { ShareAltOutlined, FileTextOutlined, FolderOutlined, FolderOpenOutlined, FilePdfOutlined, CopyOutlined, LockOutlined, CloudDownloadOutlined, CloudUploadOutlined, DownOutlined, ArrowLeftOutlined, ReloadOutlined, VerticalAlignTopOutlined } 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 rehypeRaw from 'rehype-raw'
|
import rehypeRaw from 'rehype-raw'
|
||||||
|
|
@ -15,7 +15,7 @@ import { gitPull, gitPush, getGitRepos } from '@/api/project'
|
||||||
import { getFileShareInfo, createOrUpdateFileShare, deleteFileShare } from '@/api/share'
|
import { getFileShareInfo, createOrUpdateFileShare, deleteFileShare } from '@/api/share'
|
||||||
import { searchDocuments } from '@/api/search'
|
import { searchDocuments } from '@/api/search'
|
||||||
import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
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 Toast from '@/components/Toast/Toast'
|
||||||
import ModeSwitch from '@/components/ModeSwitch/ModeSwitch'
|
import ModeSwitch from '@/components/ModeSwitch/ModeSwitch'
|
||||||
import './DocumentPage.css'
|
import './DocumentPage.css'
|
||||||
|
|
@ -45,7 +45,6 @@ function DocumentPage() {
|
||||||
const [markdownContent, setMarkdownContent] = useState('')
|
const [markdownContent, setMarkdownContent] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [openKeys, setOpenKeys] = useState([])
|
const [openKeys, setOpenKeys] = useState([])
|
||||||
const [tocCollapsed, setTocCollapsed] = useState(false)
|
|
||||||
const [tocItems, setTocItems] = useState([])
|
const [tocItems, setTocItems] = useState([])
|
||||||
const [shareModalVisible, setShareModalVisible] = useState(false)
|
const [shareModalVisible, setShareModalVisible] = useState(false)
|
||||||
const [shareInfo, setShareInfo] = useState(null)
|
const [shareInfo, setShareInfo] = useState(null)
|
||||||
|
|
@ -68,6 +67,7 @@ function DocumentPage() {
|
||||||
|
|
||||||
const contentRef = useRef(null)
|
const contentRef = useRef(null)
|
||||||
const modeSwitchingRef = useRef(false)
|
const modeSwitchingRef = useRef(false)
|
||||||
|
const [pdfToolbarTarget, setPdfToolbarTarget] = useState(null)
|
||||||
|
|
||||||
const getHeaderDisplay = (filePath) => {
|
const getHeaderDisplay = (filePath) => {
|
||||||
const resolvedPath = filePath || 'README.md'
|
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) => {
|
const resolveRelativePath = (currentPath, relativePath) => {
|
||||||
// 获取当前文件所在目录
|
// 获取当前文件所在目录
|
||||||
|
|
@ -1031,12 +1047,33 @@ function DocumentPage() {
|
||||||
{(() => {
|
{(() => {
|
||||||
const { fileName, FileIcon, isPdf } = getHeaderDisplay(selectedFile)
|
const { fileName, FileIcon, isPdf } = getHeaderDisplay(selectedFile)
|
||||||
return (
|
return (
|
||||||
<div className="docs-header-title">
|
<>
|
||||||
<span className="docs-header-item">
|
<div className="docs-header-title">
|
||||||
<FileIcon className="docs-header-icon" style={isPdf ? { color: '#f5222d' } : undefined} />
|
<span className="docs-header-item">
|
||||||
<span className="docs-header-text">{fileName}</span>
|
<FileIcon className="docs-header-icon" style={isPdf ? { color: '#f5222d' } : undefined} />
|
||||||
</span>
|
<span className="docs-header-text">{fileName}</span>
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
{viewMode === 'pdf' && <div className="docs-header-actions pdf-header-toolbar" ref={setPdfToolbarTarget} />}
|
||||||
|
{viewMode === 'markdown' && (
|
||||||
|
<Space className="docs-header-actions">
|
||||||
|
<Button
|
||||||
|
icon={<VerticalAlignTopOutlined />}
|
||||||
|
onClick={scrollContentToTop}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
回到顶部
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<CloudDownloadOutlined />}
|
||||||
|
onClick={handleExportMarkdownPDF}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
下载PDF
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1051,6 +1088,7 @@ function DocumentPage() {
|
||||||
<VirtualPDFViewer
|
<VirtualPDFViewer
|
||||||
url={pdfUrl}
|
url={pdfUrl}
|
||||||
filename={pdfFilename}
|
filename={pdfFilename}
|
||||||
|
toolbarTarget={pdfToolbarTarget}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="markdown-body">
|
<div className="markdown-body">
|
||||||
|
|
@ -1065,72 +1103,17 @@ function DocumentPage() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 浮动按钮组 - 仅在markdown模式显示 */}
|
|
||||||
{viewMode === 'markdown' && (
|
|
||||||
<DocFloatActions
|
|
||||||
scrollRef={contentRef}
|
|
||||||
right={tocCollapsed ? 24 : 280}
|
|
||||||
onExportPDF={() => {
|
|
||||||
if (!selectedFile) return
|
|
||||||
let url = getExportPdfUrl(projectId, selectedFile)
|
|
||||||
const token = localStorage.getItem('access_token')
|
|
||||||
if (token) {
|
|
||||||
url += `&token=${encodeURIComponent(token)}`
|
|
||||||
}
|
|
||||||
window.open(url, '_blank')
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Content>
|
</Content>
|
||||||
|
|
||||||
{/* 右侧TOC面板 - 仅在markdown模式显示 */}
|
{viewMode === 'markdown' && (
|
||||||
{viewMode === 'markdown' && !tocCollapsed && (
|
<FloatingToc
|
||||||
<Sider width={250} theme="light" className="docs-toc-sider">
|
items={tocItems}
|
||||||
<div className="toc-header">
|
searchKeyword={searchKeyword}
|
||||||
<h3>文档索引</h3>
|
getContainer={() => contentRef.current}
|
||||||
<Button
|
renderTitle={(item, keyword) => <HighlightText text={item.title} keyword={keyword} />}
|
||||||
type="text"
|
/>
|
||||||
size="small"
|
|
||||||
icon={<MenuFoldOutlined />}
|
|
||||||
onClick={() => setTocCollapsed(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="toc-content">
|
|
||||||
{tocItems.length > 0 ? (
|
|
||||||
<Anchor
|
|
||||||
affix={false}
|
|
||||||
offsetTop={0}
|
|
||||||
getContainer={() => contentRef.current}
|
|
||||||
items={tocItems.map((item) => ({
|
|
||||||
key: item.key,
|
|
||||||
href: item.href,
|
|
||||||
title: (
|
|
||||||
<div style={{ paddingLeft: `${(item.level - 1) * 12}px`, display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
||||||
<FileTextOutlined style={{ fontSize: '12px', color: '#8c8c8c' }} />
|
|
||||||
<HighlightText text={item.title} keyword={searchKeyword} />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="toc-empty">当前文档无标题</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Sider>
|
|
||||||
)}
|
)}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
{/* TOC展开按钮 */}
|
|
||||||
{tocCollapsed && (
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<MenuUnfoldOutlined />}
|
|
||||||
className="toc-toggle-btn"
|
|
||||||
onClick={() => setTocCollapsed(false)}
|
|
||||||
>
|
|
||||||
文档索引
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
{/* 分享模态框 */}
|
{/* 分享模态框 */}
|
||||||
|
|
|
||||||
|
|
@ -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, Button, Modal, Input, Spin, Anchor } from 'antd'
|
import { Layout, Modal, Input, Spin, Button, Space } from 'antd'
|
||||||
import { CloseOutlined, LockOutlined, MenuFoldOutlined, MenuUnfoldOutlined, FileTextOutlined, FilePdfOutlined } from '@ant-design/icons'
|
import { CloseOutlined, LockOutlined, FileTextOutlined, FilePdfOutlined, VerticalAlignTopOutlined, CloudDownloadOutlined } 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 DocFloatActions from '@/components/DocFloatActions/DocFloatActions'
|
import FloatingToc from '@/components/FloatingToc/FloatingToc'
|
||||||
import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
||||||
import {
|
import {
|
||||||
getFileSharePublicInfo,
|
getFileSharePublicInfo,
|
||||||
|
|
@ -19,17 +19,17 @@ import {
|
||||||
} from '@/api/share'
|
} from '@/api/share'
|
||||||
import './PreviewPage.css'
|
import './PreviewPage.css'
|
||||||
|
|
||||||
const { Content, Sider } = Layout
|
const { Content } = Layout
|
||||||
|
|
||||||
function FileSharePage() {
|
function FileSharePage() {
|
||||||
const { shareCode } = useParams()
|
const { shareCode } = useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const contentRef = useRef(null)
|
const contentRef = useRef(null)
|
||||||
|
const [pdfToolbarTarget, setPdfToolbarTarget] = useState(null)
|
||||||
const [shareInfo, setShareInfo] = useState(null)
|
const [shareInfo, setShareInfo] = useState(null)
|
||||||
const [contentInfo, setContentInfo] = useState(null)
|
const [contentInfo, setContentInfo] = useState(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [isMobile, setIsMobile] = useState(false)
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
const [tocCollapsed, setTocCollapsed] = useState(false)
|
|
||||||
const [tocItems, setTocItems] = useState([])
|
const [tocItems, setTocItems] = useState([])
|
||||||
const [passwordModalVisible, setPasswordModalVisible] = useState(false)
|
const [passwordModalVisible, setPasswordModalVisible] = useState(false)
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
|
|
@ -117,6 +117,12 @@ function FileSharePage() {
|
||||||
window.open(exportFileSharePDF(shareCode), '_blank')
|
window.open(exportFileSharePDF(shareCode), '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scrollContentToTop = () => {
|
||||||
|
if (contentRef.current) {
|
||||||
|
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isExternalHref = (href) => {
|
const isExternalHref = (href) => {
|
||||||
return Boolean(href && (/^[a-z][a-z\d+.-]*:/i.test(href) || href.startsWith('//')))
|
return Boolean(href && (/^[a-z][a-z\d+.-]*:/i.test(href) || href.startsWith('//')))
|
||||||
}
|
}
|
||||||
|
|
@ -174,6 +180,25 @@ function FileSharePage() {
|
||||||
<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>
|
||||||
|
{contentInfo?.type === 'markdown' && (
|
||||||
|
<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} />}
|
||||||
</div>
|
</div>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="preview-loading">
|
<div className="preview-loading">
|
||||||
|
|
@ -184,7 +209,11 @@ function FileSharePage() {
|
||||||
) : (
|
) : (
|
||||||
<div className={`preview-content-wrapper ${contentInfo?.type === 'pdf' ? 'pdf-mode' : ''}`}>
|
<div className={`preview-content-wrapper ${contentInfo?.type === 'pdf' ? 'pdf-mode' : ''}`}>
|
||||||
{contentInfo?.type === 'pdf' ? (
|
{contentInfo?.type === 'pdf' ? (
|
||||||
<VirtualPDFViewer url={contentInfo.document_url} filename={contentInfo.filename} />
|
<VirtualPDFViewer
|
||||||
|
url={contentInfo.document_url}
|
||||||
|
filename={contentInfo.filename}
|
||||||
|
toolbarTarget={pdfToolbarTarget}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="markdown-body">
|
<div className="markdown-body">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
|
|
@ -199,56 +228,15 @@ function FileSharePage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{contentInfo?.type === 'markdown' && (
|
|
||||||
<DocFloatActions
|
|
||||||
scrollRef={contentRef}
|
|
||||||
right={!isMobile && !tocCollapsed ? 280 : 24}
|
|
||||||
onExportPDF={handleExportPDF}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Content>
|
</Content>
|
||||||
|
|
||||||
{!isMobile && contentInfo?.type === 'markdown' && !tocCollapsed && (
|
{!isMobile && contentInfo?.type === 'markdown' && (
|
||||||
<Sider width={250} theme="light" className="preview-toc-sider">
|
<FloatingToc
|
||||||
<div className="toc-header">
|
items={tocItems}
|
||||||
<h3>文档索引</h3>
|
getContainer={() => contentRef.current}
|
||||||
<Button type="text" size="small" icon={<MenuFoldOutlined />} onClick={() => setTocCollapsed(true)} />
|
/>
|
||||||
</div>
|
|
||||||
<div className="toc-content">
|
|
||||||
{tocItems.length > 0 ? (
|
|
||||||
<Anchor
|
|
||||||
affix={false}
|
|
||||||
offsetTop={0}
|
|
||||||
getContainer={() => contentRef.current}
|
|
||||||
items={tocItems.map((item) => ({
|
|
||||||
key: item.key,
|
|
||||||
href: item.href,
|
|
||||||
title: (
|
|
||||||
<div style={{ paddingLeft: `${(item.level - 1) * 12}px`, display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
||||||
<FileTextOutlined style={{ fontSize: '12px', color: '#8c8c8c' }} />
|
|
||||||
<span>{item.title}</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="toc-empty">当前文档无标题</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Sider>
|
|
||||||
)}
|
)}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
{!isMobile && contentInfo?.type === 'markdown' && tocCollapsed && (
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<MenuUnfoldOutlined />}
|
|
||||||
className="toc-toggle-btn"
|
|
||||||
onClick={() => setTocCollapsed(false)}
|
|
||||||
>
|
|
||||||
文档索引
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|
|
||||||
|
|
@ -150,85 +150,6 @@
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-toc-sider {
|
|
||||||
border-left: 1px solid var(--border-color);
|
|
||||||
background: var(--bg-color-secondary) !important;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-content {
|
.preview-content {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
@ -245,12 +166,15 @@
|
||||||
background: var(--header-bg);
|
background: var(--header-bg);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-header-title {
|
.preview-header-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
@ -274,6 +198,20 @@
|
||||||
white-space: nowrap;
|
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) {
|
.preview-content-header h3:not(.preview-header-title) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
@ -494,10 +432,6 @@
|
||||||
width: 240px !important;
|
width: 240px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-toc-sider {
|
|
||||||
width: 200px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-content-wrapper {
|
.preview-content-wrapper {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|
|
||||||
|
|
@ -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, Anchor, Empty, Tooltip } from 'antd'
|
import { Layout, Menu, Spin, Button, Modal, Input, Drawer, Empty, Tooltip, Space } from 'antd'
|
||||||
import { MenuFoldOutlined, MenuUnfoldOutlined, FileTextOutlined, FolderOutlined, FolderOpenOutlined, FilePdfOutlined, LockOutlined, MenuOutlined, CloseOutlined } from '@ant-design/icons'
|
import { FileTextOutlined, FolderOutlined, FolderOpenOutlined, FilePdfOutlined, LockOutlined, MenuOutlined, CloseOutlined, VerticalAlignTopOutlined, CloudDownloadOutlined } 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 DocFloatActions from '@/components/DocFloatActions/DocFloatActions'
|
import FloatingToc from '@/components/FloatingToc/FloatingToc'
|
||||||
import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
||||||
import {
|
import {
|
||||||
getProjectSharePublicInfo,
|
getProjectSharePublicInfo,
|
||||||
|
|
@ -49,7 +49,6 @@ function ProjectSharePage() {
|
||||||
const [markdownContent, setMarkdownContent] = useState('')
|
const [markdownContent, setMarkdownContent] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [openKeys, setOpenKeys] = useState([])
|
const [openKeys, setOpenKeys] = useState([])
|
||||||
const [tocCollapsed, setTocCollapsed] = useState(false)
|
|
||||||
const [tocItems, setTocItems] = useState([])
|
const [tocItems, setTocItems] = useState([])
|
||||||
const [passwordModalVisible, setPasswordModalVisible] = useState(false)
|
const [passwordModalVisible, setPasswordModalVisible] = useState(false)
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
|
|
@ -64,6 +63,7 @@ function ProjectSharePage() {
|
||||||
const [isSearching, setIsSearching] = useState(false)
|
const [isSearching, setIsSearching] = useState(false)
|
||||||
const contentRef = useRef(null)
|
const contentRef = useRef(null)
|
||||||
const viewerRef = useRef(null)
|
const viewerRef = useRef(null)
|
||||||
|
const [pdfToolbarTarget, setPdfToolbarTarget] = useState(null)
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (window.history.length > 1) {
|
if (window.history.length > 1) {
|
||||||
|
|
@ -88,7 +88,7 @@ function ProjectSharePage() {
|
||||||
instance.mark(searchKeyword, {
|
instance.mark(searchKeyword, {
|
||||||
element: 'span',
|
element: 'span',
|
||||||
className: 'search-highlight',
|
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')
|
window.open(exportProjectSharePDF(shareCode, selectedFile), '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scrollContentToTop = () => {
|
||||||
|
if (contentRef.current) {
|
||||||
|
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const menuItems = useMemo(
|
const menuItems = useMemo(
|
||||||
() => convertTreeToMenuItems(filteredTreeData),
|
() => convertTreeToMenuItems(filteredTreeData),
|
||||||
[filteredTreeData, openKeys]
|
[filteredTreeData, openKeys]
|
||||||
|
|
@ -510,6 +516,25 @@ function ProjectSharePage() {
|
||||||
<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' && (
|
||||||
|
<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} />}
|
||||||
</div>
|
</div>
|
||||||
<div className={`preview-content-wrapper ${viewMode === 'pdf' ? 'pdf-mode' : ''}`}>
|
<div className={`preview-content-wrapper ${viewMode === 'pdf' ? 'pdf-mode' : ''}`}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|
@ -519,7 +544,7 @@ function ProjectSharePage() {
|
||||||
</Spin>
|
</Spin>
|
||||||
</div>
|
</div>
|
||||||
) : viewMode === 'pdf' ? (
|
) : viewMode === 'pdf' ? (
|
||||||
<VirtualPDFViewer url={pdfUrl} filename={pdfFilename} />
|
<VirtualPDFViewer url={pdfUrl} filename={pdfFilename} toolbarTarget={pdfToolbarTarget} />
|
||||||
) : (
|
) : (
|
||||||
<div className="markdown-body" onClick={(e) => {
|
<div className="markdown-body" onClick={(e) => {
|
||||||
if (e.defaultPrevented) return
|
if (e.defaultPrevented) return
|
||||||
|
|
@ -540,56 +565,17 @@ function ProjectSharePage() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{viewMode === 'markdown' && (
|
|
||||||
<DocFloatActions
|
|
||||||
scrollRef={contentRef}
|
|
||||||
right={!isMobile && !tocCollapsed ? 280 : 24}
|
|
||||||
onExportPDF={handleExportPDF}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Content>
|
</Content>
|
||||||
|
|
||||||
{!isMobile && viewMode === 'markdown' && !tocCollapsed && (
|
{!isMobile && viewMode === 'markdown' && (
|
||||||
<Sider width={250} theme="light" className="preview-toc-sider">
|
<FloatingToc
|
||||||
<div className="toc-header">
|
items={tocItems}
|
||||||
<h3>文档索引</h3>
|
searchKeyword={searchKeyword}
|
||||||
<Button type="text" size="small" icon={<MenuFoldOutlined />} onClick={() => setTocCollapsed(true)} />
|
getContainer={() => contentRef.current}
|
||||||
</div>
|
renderTitle={(item, keyword) => <HighlightText text={item.title} keyword={keyword} />}
|
||||||
<div className="toc-content">
|
/>
|
||||||
{tocItems.length > 0 ? (
|
|
||||||
<Anchor
|
|
||||||
affix={false}
|
|
||||||
offsetTop={0}
|
|
||||||
getContainer={() => contentRef.current}
|
|
||||||
items={tocItems.map((item) => ({
|
|
||||||
key: item.key,
|
|
||||||
href: item.href,
|
|
||||||
title: (
|
|
||||||
<div style={{ paddingLeft: `${(item.level - 1) * 12}px`, display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
||||||
<FileTextOutlined style={{ fontSize: '12px', color: '#8c8c8c' }} />
|
|
||||||
<HighlightText text={item.title} keyword={searchKeyword} />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="toc-empty">当前文档无标题</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Sider>
|
|
||||||
)}
|
)}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
{!isMobile && tocCollapsed && (
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<MenuUnfoldOutlined />}
|
|
||||||
className="toc-toggle-btn"
|
|
||||||
onClick={() => setTocCollapsed(false)}
|
|
||||||
>
|
|
||||||
文档索引
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue