调整了对超大md文档的支持
parent
9f707e879e
commit
2bb7db0d56
|
|
@ -26,6 +26,7 @@ logs/
|
|||
.env
|
||||
.env.local
|
||||
.gemini-clipboard
|
||||
.memsearch
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
|
|
|
|||
|
|
@ -7,3 +7,18 @@
|
|||
|
||||
## Session 16:54
|
||||
|
||||
|
||||
## Session 17:15
|
||||
|
||||
|
||||
## Session 17:16
|
||||
|
||||
|
||||
## Session 17:20
|
||||
|
||||
|
||||
## Session 17:54
|
||||
|
||||
|
||||
## Session 17:54
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
.large-markdown-viewer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.large-markdown-notice {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 3;
|
||||
width: min(920px, calc(100% - 48px));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.large-markdown-notice .large-markdown-alert {
|
||||
margin: 0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.markdown-body-large {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.markdown-virtual-list {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.markdown-block {
|
||||
max-width: 920px;
|
||||
width: calc(100% - 48px);
|
||||
margin: 0 auto;
|
||||
padding: 0 0 1px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.large-markdown-notice,
|
||||
.markdown-block {
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.large-markdown-notice,
|
||||
.markdown-block {
|
||||
width: calc(100% - 24px);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react'
|
||||
import { Alert } from 'antd'
|
||||
import { Virtuoso } from 'react-virtuoso'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import './LargeMarkdownViewer.css'
|
||||
|
||||
export const LARGE_MARKDOWN_THRESHOLD = 250000
|
||||
export const LARGE_MARKDOWN_NOTICE = '此文档内容过长,将采用精简模式显示'
|
||||
|
||||
export function isLargeMarkdownContent(content = '') {
|
||||
return content.length > LARGE_MARKDOWN_THRESHOLD
|
||||
}
|
||||
|
||||
export function MarkdownSizeNotice({ className = '' }) {
|
||||
return (
|
||||
<div className={`large-markdown-notice${className ? ` ${className}` : ''}`}>
|
||||
<Alert
|
||||
className="large-markdown-alert"
|
||||
type="info"
|
||||
showIcon
|
||||
message={LARGE_MARKDOWN_NOTICE}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function splitMarkdownIntoBlocks(content) {
|
||||
if (!content) return []
|
||||
|
||||
const blocks = []
|
||||
const lines = content.split('\n')
|
||||
let current = []
|
||||
let inFence = false
|
||||
|
||||
const flush = () => {
|
||||
if (current.length > 0) {
|
||||
blocks.push(current.join('\n'))
|
||||
current = []
|
||||
}
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
const isFenceLine = /^\s*(```|~~~)/.test(line)
|
||||
const isHeading = /^(#{1,6})\s+/.test(line)
|
||||
|
||||
if (!inFence && isHeading) {
|
||||
flush()
|
||||
}
|
||||
|
||||
current.push(line)
|
||||
|
||||
if (isFenceLine) {
|
||||
inFence = !inFence
|
||||
}
|
||||
|
||||
if (!inFence && line.trim() === '') {
|
||||
flush()
|
||||
}
|
||||
}
|
||||
|
||||
flush()
|
||||
return blocks
|
||||
}
|
||||
|
||||
const LargeMarkdownViewer = forwardRef(function LargeMarkdownViewer({ content, components, onClick }, ref) {
|
||||
const virtuosoRef = useRef(null)
|
||||
const markdownBlocks = useMemo(() => splitMarkdownIntoBlocks(content), [content])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
scrollToTop: () => {
|
||||
virtuosoRef.current?.scrollToIndex({
|
||||
index: 0,
|
||||
align: 'start',
|
||||
behavior: 'smooth',
|
||||
})
|
||||
},
|
||||
}), [])
|
||||
|
||||
return (
|
||||
<div className="large-markdown-viewer">
|
||||
<MarkdownSizeNotice />
|
||||
<div className="markdown-body markdown-body-large" onClick={onClick}>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
className="markdown-virtual-list"
|
||||
data={markdownBlocks}
|
||||
itemContent={(index, block) => (
|
||||
<div className="markdown-block" data-index={index}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeRaw, rehypeSlug]}
|
||||
components={components}
|
||||
>
|
||||
{block}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default LargeMarkdownViewer
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import { forwardRef, useMemo } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import rehypeHighlight from 'rehype-highlight'
|
||||
|
||||
const MarkdownViewer = forwardRef(function MarkdownViewer(
|
||||
{
|
||||
content = '',
|
||||
components,
|
||||
onClick,
|
||||
className = '',
|
||||
allowRaw = false,
|
||||
enableHighlight = true,
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const rehypePlugins = useMemo(() => {
|
||||
const plugins = []
|
||||
if (allowRaw) {
|
||||
plugins.push(rehypeRaw)
|
||||
}
|
||||
plugins.push(rehypeSlug)
|
||||
if (enableHighlight) {
|
||||
plugins.push(rehypeHighlight)
|
||||
}
|
||||
return plugins
|
||||
}, [allowRaw, enableHighlight])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`markdown-body${className ? ` ${className}` : ''}`}
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={rehypePlugins}
|
||||
components={components}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default MarkdownViewer
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
@import '@/components/LargeMarkdownViewer/LargeMarkdownViewer.css';
|
||||
|
||||
.document-editor-page {
|
||||
height: calc(100vh - 64px);
|
||||
/* width: calc(100% + 32px); */
|
||||
|
|
@ -232,6 +234,15 @@
|
|||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.bytemd-wrapper.large-file-editor {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.large-file-editor-notice {
|
||||
top: 12px;
|
||||
}
|
||||
|
||||
/* Fix for bytemd-react wrapper div */
|
||||
.bytemd-wrapper>div {
|
||||
flex: 1;
|
||||
|
|
@ -242,6 +253,36 @@
|
|||
min-width: 0;
|
||||
}
|
||||
|
||||
.bytemd-wrapper.large-file-editor>.large-markdown-notice {
|
||||
flex: none !important;
|
||||
display: block !important;
|
||||
width: min(920px, calc(100% - 48px)) !important;
|
||||
height: auto !important;
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
.large-markdown-textarea {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
resize: none;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 2px;
|
||||
padding: 64px 16px 16px;
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.large-markdown-textarea:focus {
|
||||
border-color: #1677ff;
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.15);
|
||||
}
|
||||
|
||||
.empty-editor {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {
|
|||
FileOutlined,
|
||||
FolderOutlined,
|
||||
FolderOpenOutlined,
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
SaveOutlined,
|
||||
|
|
@ -14,7 +13,6 @@ import {
|
|||
UploadOutlined,
|
||||
DownloadOutlined,
|
||||
SwapOutlined,
|
||||
FileImageOutlined,
|
||||
FilePdfOutlined,
|
||||
FileTextOutlined,
|
||||
UndoOutlined,
|
||||
|
|
@ -35,11 +33,12 @@ import {
|
|||
operateFile,
|
||||
uploadFile,
|
||||
importDocuments,
|
||||
exportDirectory,
|
||||
uploadDocument,
|
||||
getDocumentUrl,
|
||||
} from '@/api/file'
|
||||
import Toast from '@/components/Toast/Toast'
|
||||
import ModeSwitch from '@/components/ModeSwitch/ModeSwitch'
|
||||
import { MarkdownSizeNotice, isLargeMarkdownContent } from '@/components/LargeMarkdownViewer/LargeMarkdownViewer'
|
||||
import './DocumentEditor.css'
|
||||
|
||||
const { Sider, Content } = Layout
|
||||
|
|
@ -63,7 +62,6 @@ function DocumentEditor() {
|
|||
const [creationParentPath, setCreationParentPath] = useState('')
|
||||
const [moveTargetPath, setMoveTargetPath] = useState('')
|
||||
const [dirOptions, setDirOptions] = useState([])
|
||||
const [editorHeight, setEditorHeight] = useState(600) // 设置初始高度为600px
|
||||
const [openKeys, setOpenKeys] = useState([]) // Menu组件的展开项
|
||||
const [uploadProgress, setUploadProgress] = useState(0) // 上传进度
|
||||
const [uploading, setUploading] = useState(false) // 是否正在上传
|
||||
|
|
@ -78,6 +76,7 @@ function DocumentEditor() {
|
|||
const [modeSwitchValue, setModeSwitchValue] = useState('edit')
|
||||
const editorCtxRef = useRef(null)
|
||||
const modeSwitchingRef = useRef(false)
|
||||
const isLargeMarkdown = isLargeMarkdownContent(fileContent)
|
||||
|
||||
const isHeaderPdf = selectedFile?.toLowerCase().endsWith('.pdf')
|
||||
const HeaderIcon = isHeaderPdf ? FilePdfOutlined : FileTextOutlined
|
||||
|
|
@ -109,16 +108,139 @@ function DocumentEditor() {
|
|||
navigate(to)
|
||||
}
|
||||
|
||||
const updateFileParam = (filePath) => {
|
||||
const updateSelectedParam = (path, isFile = true) => {
|
||||
const nextParams = new URLSearchParams(searchParams)
|
||||
if (filePath) {
|
||||
nextParams.set('file', filePath)
|
||||
} else {
|
||||
nextParams.delete('file')
|
||||
nextParams.delete('selected')
|
||||
|
||||
if (path) {
|
||||
nextParams.set(isFile ? 'file' : 'selected', path)
|
||||
}
|
||||
setSearchParams(nextParams, { replace: true })
|
||||
}
|
||||
|
||||
const updateFileParam = (filePath) => {
|
||||
updateSelectedParam(filePath, true)
|
||||
}
|
||||
|
||||
const buildDocumentUrl = (filePath) => {
|
||||
const params = new URLSearchParams()
|
||||
const token = localStorage.getItem('access_token')
|
||||
|
||||
if (token) {
|
||||
params.set('token', token)
|
||||
}
|
||||
|
||||
const query = params.toString()
|
||||
return `${getDocumentUrl(projectId, filePath)}${query ? `?${query}` : ''}`
|
||||
}
|
||||
|
||||
const downloadByUrl = (url, filename) => {
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
const downloadBlob = (blob, filename) => {
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
downloadByUrl(url, filename)
|
||||
window.URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
const getSelectedDownloadNode = () => {
|
||||
if (selectedNode) return selectedNode
|
||||
if (selectedMenuKey) return findNodeByKey(treeData, selectedMenuKey)
|
||||
if (selectedFile) return findNodeByKey(treeData, selectedFile)
|
||||
return null
|
||||
}
|
||||
|
||||
const getDownloadFilename = (path) => path.split('/').filter(Boolean).pop() || 'document'
|
||||
|
||||
const downloadMarkdownFile = () => {
|
||||
const filename = getDownloadFilename(selectedFile)
|
||||
downloadBlob(new Blob([fileContent], { type: 'text/markdown;charset=utf-8' }), filename)
|
||||
}
|
||||
|
||||
const handleDownloadSelectedFile = () => {
|
||||
const targetNode = getSelectedDownloadNode()
|
||||
|
||||
if (!targetNode) {
|
||||
Toast.warning('提示', '请先选择文件')
|
||||
return
|
||||
}
|
||||
|
||||
if (!targetNode.isLeaf) {
|
||||
Toast.warning('暂不支持文件夹下载', '请选择 Markdown 或 PDF 文件')
|
||||
return
|
||||
}
|
||||
|
||||
const path = targetNode.key
|
||||
const lowerPath = path.toLowerCase()
|
||||
|
||||
if (!lowerPath.endsWith('.md') && !lowerPath.endsWith('.pdf')) {
|
||||
Toast.warning('暂不支持该文件类型', '请选择 Markdown 或 PDF 文件')
|
||||
return
|
||||
}
|
||||
|
||||
if (lowerPath.endsWith('.md')) {
|
||||
if (selectedFile === path) {
|
||||
downloadMarkdownFile()
|
||||
} else {
|
||||
getFileContent(projectId, path).then((res) => {
|
||||
downloadBlob(
|
||||
new Blob([res.data?.content || ''], { type: 'text/markdown;charset=utf-8' }),
|
||||
getDownloadFilename(path)
|
||||
)
|
||||
}).catch((error) => {
|
||||
console.error('Download markdown error:', error)
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
downloadByUrl(buildDocumentUrl(path), getDownloadFilename(path))
|
||||
}
|
||||
|
||||
const selectFolder = (folderPath, { syncUrl = false } = {}) => {
|
||||
const targetNode = findNodeByKey(treeData, folderPath)
|
||||
if (targetNode) {
|
||||
setSelectedNode(targetNode)
|
||||
}
|
||||
setSelectedMenuKey(folderPath)
|
||||
setSelectedFile(null)
|
||||
setIsPdfSelected(false)
|
||||
setFileContent('')
|
||||
|
||||
if (syncUrl) {
|
||||
updateSelectedParam(folderPath, false)
|
||||
}
|
||||
}
|
||||
|
||||
const getModeSwitchTarget = () => {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
if (selectedFile) {
|
||||
params.set('file', selectedFile)
|
||||
} else if (selectedMenuKey) {
|
||||
const targetNode = findNodeByKey(treeData, selectedMenuKey)
|
||||
if (targetNode && !targetNode.isLeaf) {
|
||||
params.set('selected', selectedMenuKey)
|
||||
}
|
||||
} else {
|
||||
const selectedParam = searchParams.get('selected')
|
||||
if (selectedParam) {
|
||||
params.set('selected', selectedParam)
|
||||
}
|
||||
}
|
||||
|
||||
const query = params.toString()
|
||||
return `/projects/${projectId}/docs${query ? `?${query}` : ''}`
|
||||
}
|
||||
|
||||
const encodeMarkdownLinkTarget = (targetPath) => {
|
||||
if (!targetPath) return targetPath
|
||||
|
||||
|
|
@ -159,22 +281,6 @@ function DocumentEditor() {
|
|||
setLinkTarget(null)
|
||||
}
|
||||
|
||||
// 在组件挂载后立即计算正确的高度
|
||||
useEffect(() => {
|
||||
const calculateHeight = () => {
|
||||
const windowHeight = window.innerHeight
|
||||
const newHeight = Math.max(windowHeight - 180, 400) // 最小高度400px
|
||||
setEditorHeight(newHeight)
|
||||
}
|
||||
|
||||
// 立即执行一次
|
||||
calculateHeight()
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', calculateHeight)
|
||||
return () => window.removeEventListener('resize', calculateHeight)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchTree()
|
||||
}, [projectId])
|
||||
|
|
@ -210,32 +316,6 @@ function DocumentEditor() {
|
|||
})
|
||||
}
|
||||
|
||||
const handleSelectFile = async (selectedKeys, info) => {
|
||||
// 记录选中的节点(无论是文件还是目录)
|
||||
setSelectedNode(info.node)
|
||||
|
||||
if (info.node.isLeaf) {
|
||||
const filePath = selectedKeys[0]
|
||||
|
||||
// 检查是否是PDF文件
|
||||
if (filePath.toLowerCase().endsWith('.pdf')) {
|
||||
Toast.info('提示', 'PDF文件请在浏览模式下查看')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getFileContent(projectId, filePath)
|
||||
setSelectedFile(filePath)
|
||||
setFileContent(res.data.content)
|
||||
} catch (error) {
|
||||
// 错误已通过request interceptor处理
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 查找树节点的辅助函数
|
||||
const findNodeByKey = (nodes, key) => {
|
||||
for (const node of nodes) {
|
||||
|
|
@ -271,8 +351,11 @@ function DocumentEditor() {
|
|||
setSelectedMenuKey(key)
|
||||
|
||||
if (!targetNode.isLeaf) {
|
||||
setSelectedFile(null)
|
||||
setIsPdfSelected(false)
|
||||
setFileContent('')
|
||||
if (syncUrl) {
|
||||
updateFileParam(null)
|
||||
updateSelectedParam(key, false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -306,6 +389,16 @@ function DocumentEditor() {
|
|||
if (treeData.length === 0) return
|
||||
|
||||
const fileParam = searchParams.get('file')
|
||||
const selectedParam = searchParams.get('selected')
|
||||
|
||||
if (selectedParam) {
|
||||
const targetNode = findNodeByKey(treeData, selectedParam)
|
||||
if (targetNode && !targetNode.isLeaf && selectedParam !== selectedMenuKey) {
|
||||
selectFolder(selectedParam)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!fileParam) return
|
||||
if (fileParam === selectedFile) return
|
||||
|
||||
|
|
@ -651,42 +744,6 @@ function DocumentEditor() {
|
|||
}
|
||||
}
|
||||
|
||||
// 导出目录
|
||||
const handleExportDirectory = async () => {
|
||||
// 如果选中了目录,导出该目录;否则导出整个项目
|
||||
const directoryPath = selectedNode && !selectedNode.isLeaf ? selectedNode.key : ''
|
||||
|
||||
try {
|
||||
const response = await exportDirectory(projectId, directoryPath)
|
||||
|
||||
// 从响应头中提取文件名
|
||||
const contentDisposition = response.headers['content-disposition']
|
||||
let filename = `${directoryPath || 'root'}.zip`
|
||||
if (contentDisposition) {
|
||||
const matches = /filename=(.+)/.exec(contentDisposition)
|
||||
if (matches && matches[1]) {
|
||||
filename = matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
// 创建blob URL并触发下载
|
||||
const url = window.URL.createObjectURL(response.data)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
Toast.success('成功', '导出成功')
|
||||
} catch (error) {
|
||||
console.error('Export error:', error)
|
||||
Toast.error('错误', '导出失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 移动文件/目录
|
||||
const handleMove = (path) => {
|
||||
setRightClickNode(path)
|
||||
|
|
@ -972,8 +1029,7 @@ function DocumentEditor() {
|
|||
children: node.children ? convertTreeToMenuItems(node.children) : [],
|
||||
className: isSelected ? 'folder-selected' : '',
|
||||
onTitleClick: () => {
|
||||
setSelectedNode(node)
|
||||
setSelectedMenuKey(node.key)
|
||||
selectFolder(node.key, { syncUrl: true })
|
||||
},
|
||||
}
|
||||
} else if (node.title && node.title.endsWith('.md')) {
|
||||
|
|
@ -1033,12 +1089,7 @@ function DocumentEditor() {
|
|||
modeSwitchingRef.current = true
|
||||
setModeSwitchValue('view')
|
||||
setTimeout(() => {
|
||||
const params = new URLSearchParams()
|
||||
if (selectedFile) {
|
||||
params.set('file', selectedFile)
|
||||
}
|
||||
const query = params.toString()
|
||||
navigateWithTransition(`/projects/${projectId}/docs${query ? `?${query}` : ''}`)
|
||||
navigateWithTransition(getModeSwitchTarget())
|
||||
}, 160)
|
||||
}
|
||||
}}
|
||||
|
|
@ -1073,11 +1124,11 @@ function DocumentEditor() {
|
|||
/>
|
||||
</Tooltip>
|
||||
</Upload>
|
||||
<Tooltip title="导出文档">
|
||||
<Tooltip title="下载选中文件">
|
||||
<Button
|
||||
size="middle"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={handleExportDirectory}
|
||||
onClick={handleDownloadSelectedFile}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space.Compact>
|
||||
|
|
@ -1135,11 +1186,22 @@ function DocumentEditor() {
|
|||
</div>
|
||||
) : selectedFile ? (
|
||||
<div
|
||||
className="bytemd-wrapper"
|
||||
className={`bytemd-wrapper ${isLargeMarkdown ? 'large-file-editor' : ''}`}
|
||||
onPaste={handlePaste}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
>
|
||||
{isLargeMarkdown ? (
|
||||
<>
|
||||
<MarkdownSizeNotice className="large-file-editor-notice" />
|
||||
<textarea
|
||||
className="large-markdown-textarea"
|
||||
value={fileContent}
|
||||
onChange={(e) => setFileContent(e.target.value)}
|
||||
spellCheck={false}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Editor
|
||||
key={selectedFile}
|
||||
value={fileContent}
|
||||
|
|
@ -1152,6 +1214,7 @@ function DocumentEditor() {
|
|||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="empty-editor">
|
||||
|
|
|
|||
|
|
@ -244,6 +244,13 @@
|
|||
min-height: 100%;
|
||||
}
|
||||
|
||||
.docs-content-wrapper.large-markdown-mode {
|
||||
max-width: 100%;
|
||||
height: calc(100% - 57px);
|
||||
min-height: 0;
|
||||
padding: 0 0 24px;
|
||||
}
|
||||
|
||||
/* PDF模式下使用全宽 */
|
||||
.docs-content-wrapper.pdf-mode {
|
||||
max-width: 100%;
|
||||
|
|
@ -260,6 +267,16 @@
|
|||
min-height: 400px;
|
||||
}
|
||||
|
||||
.docs-folder-placeholder {
|
||||
min-height: 360px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useEffect, useRef, useMemo } from 'react'
|
||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { Layout, Menu, Spin, Button, Tooltip, message, Modal, Input, Space, Dropdown, Empty, Switch } from 'antd'
|
||||
import { ShareAltOutlined, FileTextOutlined, FolderOutlined, FolderOpenOutlined, FilePdfOutlined, CopyOutlined, LockOutlined, CloudDownloadOutlined, CloudUploadOutlined, DownOutlined, ArrowLeftOutlined, ReloadOutlined, VerticalAlignTopOutlined } from '@ant-design/icons'
|
||||
import { ShareAltOutlined, FileTextOutlined, FolderOutlined, FolderOpenOutlined, FilePdfOutlined, CopyOutlined, CloudDownloadOutlined, CloudUploadOutlined, ArrowLeftOutlined, ReloadOutlined, VerticalAlignTopOutlined } from '@ant-design/icons'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
|
|
@ -18,9 +18,11 @@ import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
|||
import FloatingToc from '@/components/FloatingToc/FloatingToc'
|
||||
import Toast from '@/components/Toast/Toast'
|
||||
import ModeSwitch from '@/components/ModeSwitch/ModeSwitch'
|
||||
import LargeMarkdownViewer, { isLargeMarkdownContent } from '@/components/LargeMarkdownViewer/LargeMarkdownViewer'
|
||||
import './DocumentPage.css'
|
||||
|
||||
const { Sider, Content } = Layout
|
||||
const MAX_TOC_ITEMS = 500
|
||||
|
||||
// 高亮渲染组件
|
||||
const HighlightText = ({ text, keyword }) => {
|
||||
|
|
@ -51,7 +53,6 @@ function DocumentPage() {
|
|||
const [hasPassword, setHasPassword] = useState(false)
|
||||
const [password, setPassword] = useState('')
|
||||
const [userRole, setUserRole] = useState('viewer')
|
||||
const [pdfViewerVisible, setPdfViewerVisible] = useState(false)
|
||||
const [pdfUrl, setPdfUrl] = useState('')
|
||||
const [pdfFilename, setPdfFilename] = useState('')
|
||||
const [viewMode, setViewMode] = useState('markdown')
|
||||
|
|
@ -66,14 +67,18 @@ function DocumentPage() {
|
|||
const [modeSwitchValue, setModeSwitchValue] = useState('view')
|
||||
|
||||
const contentRef = useRef(null)
|
||||
const largeMarkdownRef = useRef(null)
|
||||
const modeSwitchingRef = useRef(false)
|
||||
const [pdfToolbarTarget, setPdfToolbarTarget] = useState(null)
|
||||
const isLargeMarkdown = isLargeMarkdownContent(markdownContent)
|
||||
|
||||
const getHeaderDisplay = (filePath) => {
|
||||
const resolvedPath = filePath || 'README.md'
|
||||
const resolvedPath = selectedNodeKey || filePath || 'README.md'
|
||||
const fileName = resolvedPath.split('/').filter(Boolean).pop() || 'README.md'
|
||||
const selectedNode = selectedNodeKey ? findNodeByKey(fileTree, selectedNodeKey) : null
|
||||
const isFolder = Boolean(selectedNode && !selectedNode.isLeaf)
|
||||
const isPdf = fileName.toLowerCase().endsWith('.pdf')
|
||||
const FileIcon = isPdf ? FilePdfOutlined : FileTextOutlined
|
||||
const FileIcon = isFolder ? FolderOutlined : isPdf ? FilePdfOutlined : FileTextOutlined
|
||||
|
||||
return {
|
||||
fileName,
|
||||
|
|
@ -90,16 +95,68 @@ function DocumentPage() {
|
|||
navigate(to)
|
||||
}
|
||||
|
||||
const updateFileParam = (filePath) => {
|
||||
const updateSelectedParam = (path, isFile = true) => {
|
||||
const nextParams = new URLSearchParams(searchParams)
|
||||
if (filePath) {
|
||||
nextParams.set('file', filePath)
|
||||
} else {
|
||||
nextParams.delete('file')
|
||||
nextParams.delete('selected')
|
||||
|
||||
if (path) {
|
||||
nextParams.set(isFile ? 'file' : 'selected', path)
|
||||
}
|
||||
setSearchParams(nextParams, { replace: true })
|
||||
}
|
||||
|
||||
const updateFileParam = (filePath) => {
|
||||
updateSelectedParam(filePath, true)
|
||||
}
|
||||
|
||||
const expandParentFolders = (path) => {
|
||||
const parts = path.split('/')
|
||||
const allParentPaths = []
|
||||
let currentPath = ''
|
||||
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i]
|
||||
allParentPaths.push(currentPath)
|
||||
}
|
||||
|
||||
if (allParentPaths.length > 0) {
|
||||
setOpenKeys(prev => [...new Set([...prev, ...allParentPaths])])
|
||||
}
|
||||
}
|
||||
|
||||
const selectFolder = (folderPath, { syncUrl = false } = {}) => {
|
||||
setSelectedFile('')
|
||||
setSelectedNodeKey(folderPath)
|
||||
setMarkdownContent('')
|
||||
setTocItems([])
|
||||
setViewMode('folder')
|
||||
expandParentFolders(`${folderPath}/placeholder`)
|
||||
|
||||
if (syncUrl) {
|
||||
updateSelectedParam(folderPath, false)
|
||||
}
|
||||
}
|
||||
|
||||
const openDocumentPath = (filePath, { syncUrl = false } = {}) => {
|
||||
setSelectedFile(filePath)
|
||||
setSelectedNodeKey(filePath)
|
||||
expandParentFolders(filePath)
|
||||
|
||||
if (syncUrl) {
|
||||
updateFileParam(filePath)
|
||||
}
|
||||
|
||||
if (filePath.toLowerCase().endsWith('.pdf')) {
|
||||
setPdfUrl(buildDocumentUrl(filePath))
|
||||
setPdfFilename(filePath.split('/').pop())
|
||||
setViewMode('pdf')
|
||||
} else {
|
||||
setViewMode('markdown')
|
||||
loadMarkdown(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
const buildDocumentUrl = (filePath, refreshKey = null) => {
|
||||
const params = new URLSearchParams()
|
||||
const token = localStorage.getItem('access_token')
|
||||
|
|
@ -141,6 +198,7 @@ function DocumentPage() {
|
|||
if (fileTree.length === 0) return
|
||||
|
||||
const fileParam = searchParams.get('file')
|
||||
const selectedParam = searchParams.get('selected')
|
||||
const keywordParam = searchParams.get('keyword')
|
||||
|
||||
// 处理搜索
|
||||
|
|
@ -149,42 +207,24 @@ function DocumentPage() {
|
|||
}
|
||||
|
||||
// 处理文件加载
|
||||
if (fileParam) {
|
||||
if (fileParam !== selectedFile) {
|
||||
setSelectedFile(fileParam)
|
||||
setSelectedNodeKey(fileParam)
|
||||
|
||||
// 展开父目录
|
||||
const parts = fileParam.split('/')
|
||||
const allParentPaths = []
|
||||
let currentPath = ''
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i]
|
||||
allParentPaths.push(currentPath)
|
||||
}
|
||||
if (allParentPaths.length > 0) {
|
||||
setOpenKeys(prev => [...new Set([...prev, ...allParentPaths])])
|
||||
}
|
||||
|
||||
// 处理 PDF 或 Markdown
|
||||
if (fileParam.toLowerCase().endsWith('.pdf')) {
|
||||
setPdfUrl(buildDocumentUrl(fileParam))
|
||||
setPdfFilename(fileParam.split('/').pop())
|
||||
setViewMode('pdf')
|
||||
} else {
|
||||
loadMarkdown(fileParam)
|
||||
setViewMode('markdown')
|
||||
if (selectedParam) {
|
||||
const targetNode = findNodeByKey(fileTree, selectedParam)
|
||||
if (targetNode && !targetNode.isLeaf && selectedParam !== selectedNodeKey) {
|
||||
selectFolder(selectedParam)
|
||||
}
|
||||
} else if (fileParam) {
|
||||
const targetNode = findNodeByKey(fileTree, fileParam)
|
||||
if (targetNode && !targetNode.isLeaf) {
|
||||
selectFolder(fileParam, { syncUrl: true })
|
||||
} else if (fileParam !== selectedFile) {
|
||||
openDocumentPath(fileParam)
|
||||
}
|
||||
} else {
|
||||
// 如果没有指定文件,且当前没有选中文件,默认打开 README.md
|
||||
if (!selectedFile) {
|
||||
if (!selectedFile && !selectedNodeKey) {
|
||||
const readmeNode = findReadme(fileTree)
|
||||
if (readmeNode) {
|
||||
setSelectedFile(readmeNode.key)
|
||||
setSelectedNodeKey(readmeNode.key)
|
||||
updateFileParam(readmeNode.key)
|
||||
loadMarkdown(readmeNode.key)
|
||||
openDocumentPath(readmeNode.key, { syncUrl: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -325,7 +365,7 @@ function DocumentPage() {
|
|||
key: node.key,
|
||||
label: labelNode,
|
||||
icon: isOpen ? <FolderOpenOutlined /> : <FolderOutlined />,
|
||||
onTitleClick: () => setSelectedNodeKey(node.key),
|
||||
onTitleClick: () => selectFolder(node.key, { syncUrl: true }),
|
||||
children: node.children ? convertTreeToMenuItems(node.children) : [],
|
||||
}
|
||||
} else if (node.title && node.title.endsWith('.md')) {
|
||||
|
|
@ -357,7 +397,7 @@ function DocumentPage() {
|
|||
|
||||
// 滚动到顶部
|
||||
if (contentRef.current) {
|
||||
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
contentRef.current.scrollTo({ top: 0, behavior: 'auto' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load markdown error:', error)
|
||||
|
|
@ -369,12 +409,19 @@ function DocumentPage() {
|
|||
|
||||
// 提取 markdown 标题生成目录
|
||||
useEffect(() => {
|
||||
if (markdownContent) {
|
||||
if (markdownContent && !isLargeMarkdown) {
|
||||
let canceled = false
|
||||
const schedule = window.requestIdleCallback || ((cb) => window.setTimeout(cb, 1))
|
||||
const cancel = window.cancelIdleCallback || window.clearTimeout
|
||||
|
||||
const taskId = schedule(() => {
|
||||
if (canceled) return
|
||||
|
||||
const slugger = new GithubSlugger()
|
||||
const headings = []
|
||||
const lines = markdownContent.split('\n')
|
||||
|
||||
lines.forEach((line) => {
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^(#{1,6})\s+(.+)$/)
|
||||
if (match) {
|
||||
const level = match[1].length
|
||||
|
|
@ -388,33 +435,44 @@ function DocumentPage() {
|
|||
title,
|
||||
level,
|
||||
})
|
||||
|
||||
if (headings.length >= MAX_TOC_ITEMS) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setTocItems(headings)
|
||||
})
|
||||
|
||||
return () => {
|
||||
canceled = true
|
||||
cancel(taskId)
|
||||
}
|
||||
}, [markdownContent])
|
||||
} else {
|
||||
setTocItems([])
|
||||
}
|
||||
}, [markdownContent, isLargeMarkdown])
|
||||
|
||||
// 处理菜单点击
|
||||
const handleMenuClick = ({ key }) => {
|
||||
setSelectedFile(key)
|
||||
setSelectedNodeKey(key)
|
||||
updateFileParam(key)
|
||||
const node = findNodeByKey(fileTree, key)
|
||||
if (!node) return
|
||||
|
||||
// 检查是否是PDF文件
|
||||
if (key.toLowerCase().endsWith('.pdf')) {
|
||||
// 显示PDF - 添加token到URL
|
||||
setPdfUrl(buildDocumentUrl(key))
|
||||
setPdfFilename(key.split('/').pop())
|
||||
setViewMode('pdf')
|
||||
} else {
|
||||
// 加载Markdown文件
|
||||
setViewMode('markdown')
|
||||
loadMarkdown(key)
|
||||
if (!node.isLeaf) {
|
||||
selectFolder(key, { syncUrl: true })
|
||||
return
|
||||
}
|
||||
|
||||
openDocumentPath(key, { syncUrl: true })
|
||||
}
|
||||
|
||||
const scrollContentToTop = () => {
|
||||
if (isLargeMarkdown) {
|
||||
largeMarkdownRef.current?.scrollToTop()
|
||||
return
|
||||
}
|
||||
|
||||
if (contentRef.current) {
|
||||
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
|
@ -712,6 +770,8 @@ function DocumentPage() {
|
|||
const params = new URLSearchParams()
|
||||
if (selectedFile) {
|
||||
params.set('file', selectedFile)
|
||||
} else if (selectedNodeKey) {
|
||||
params.set('selected', selectedNodeKey)
|
||||
}
|
||||
const query = params.toString()
|
||||
navigateWithTransition(`/projects/${projectId}/editor${query ? `?${query}` : ''}`)
|
||||
|
|
@ -1077,7 +1137,7 @@ function DocumentPage() {
|
|||
)
|
||||
})()}
|
||||
</div>
|
||||
<div className={`docs-content-wrapper ${viewMode === 'pdf' ? 'pdf-mode' : ''}`}>
|
||||
<div className={`docs-content-wrapper ${viewMode === 'pdf' ? 'pdf-mode' : ''} ${isLargeMarkdown ? 'large-markdown-mode' : ''}`}>
|
||||
{loading ? (
|
||||
<div className="docs-loading">
|
||||
<Spin size="large">
|
||||
|
|
@ -1090,6 +1150,17 @@ function DocumentPage() {
|
|||
filename={pdfFilename}
|
||||
toolbarTarget={pdfToolbarTarget}
|
||||
/>
|
||||
) : viewMode === 'folder' ? (
|
||||
<div className="docs-folder-placeholder">
|
||||
<FolderOpenOutlined />
|
||||
<span>已选择文件夹,请从左侧选择 Markdown 或 PDF 文件查看。</span>
|
||||
</div>
|
||||
) : isLargeMarkdown ? (
|
||||
<LargeMarkdownViewer
|
||||
ref={largeMarkdownRef}
|
||||
content={markdownContent}
|
||||
components={markdownComponents}
|
||||
/>
|
||||
) : (
|
||||
<div className="markdown-body">
|
||||
<ReactMarkdown
|
||||
|
|
@ -1105,7 +1176,7 @@ function DocumentPage() {
|
|||
|
||||
</Content>
|
||||
|
||||
{viewMode === 'markdown' && (
|
||||
{viewMode === 'markdown' && !isLargeMarkdown && (
|
||||
<FloatingToc
|
||||
items={tocItems}
|
||||
searchKeyword={searchKeyword}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import GithubSlugger from 'github-slugger'
|
|||
import Toast from '@/components/Toast/Toast'
|
||||
import FloatingToc, { TocDrawer } from '@/components/FloatingToc/FloatingToc'
|
||||
import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
||||
import LargeMarkdownViewer, { isLargeMarkdownContent } from '@/components/LargeMarkdownViewer/LargeMarkdownViewer'
|
||||
import {
|
||||
getFileSharePublicInfo,
|
||||
verifyFileSharePassword,
|
||||
|
|
@ -25,6 +26,7 @@ function FileSharePage() {
|
|||
const { shareCode } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const contentRef = useRef(null)
|
||||
const largeMarkdownRef = useRef(null)
|
||||
const [pdfToolbarTarget, setPdfToolbarTarget] = useState(null)
|
||||
const [shareInfo, setShareInfo] = useState(null)
|
||||
const [contentInfo, setContentInfo] = useState(null)
|
||||
|
|
@ -34,6 +36,8 @@ function FileSharePage() {
|
|||
const [tocDrawerVisible, setTocDrawerVisible] = useState(false)
|
||||
const [passwordModalVisible, setPasswordModalVisible] = useState(false)
|
||||
const [password, setPassword] = useState('')
|
||||
const markdownContent = contentInfo?.type === 'markdown' ? (contentInfo.content || '') : ''
|
||||
const isLargeMarkdown = isLargeMarkdownContent(markdownContent)
|
||||
|
||||
useEffect(() => {
|
||||
loadFileShare()
|
||||
|
|
@ -47,8 +51,7 @@ function FileSharePage() {
|
|||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const markdownContent = contentInfo?.type === 'markdown' ? (contentInfo.content || '') : ''
|
||||
if (!markdownContent) {
|
||||
if (!markdownContent || isLargeMarkdown) {
|
||||
setTocItems([])
|
||||
return
|
||||
}
|
||||
|
|
@ -64,7 +67,7 @@ function FileSharePage() {
|
|||
headings.push({ key: `#${key}`, href: `#${key}`, title, level })
|
||||
})
|
||||
setTocItems(headings)
|
||||
}, [contentInfo])
|
||||
}, [markdownContent, isLargeMarkdown])
|
||||
|
||||
const handleClose = () => {
|
||||
if (window.history.length > 1) {
|
||||
|
|
@ -119,6 +122,11 @@ function FileSharePage() {
|
|||
}
|
||||
|
||||
const scrollContentToTop = () => {
|
||||
if (isLargeMarkdown) {
|
||||
largeMarkdownRef.current?.scrollToTop()
|
||||
return
|
||||
}
|
||||
|
||||
if (contentRef.current) {
|
||||
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
|
@ -202,6 +210,7 @@ function FileSharePage() {
|
|||
aria-label="下载PDF"
|
||||
/>
|
||||
</Tooltip>
|
||||
{!isLargeMarkdown && (
|
||||
<Tooltip title="文档索引">
|
||||
<Button
|
||||
icon={<MenuOutlined />}
|
||||
|
|
@ -211,6 +220,7 @@ function FileSharePage() {
|
|||
aria-label="文档索引"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
) : (
|
||||
<Space className="preview-header-actions">
|
||||
|
|
@ -240,7 +250,7 @@ function FileSharePage() {
|
|||
</Spin>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`preview-content-wrapper ${contentInfo?.type === 'pdf' ? 'pdf-mode' : ''}`}>
|
||||
<div className={`preview-content-wrapper ${contentInfo?.type === 'pdf' ? 'pdf-mode' : ''} ${isLargeMarkdown ? 'large-markdown-mode' : ''}`}>
|
||||
{contentInfo?.type === 'pdf' ? (
|
||||
<VirtualPDFViewer
|
||||
url={contentInfo.document_url}
|
||||
|
|
@ -248,6 +258,12 @@ function FileSharePage() {
|
|||
toolbarTarget={pdfToolbarTarget}
|
||||
compactToolbar={isMobile}
|
||||
/>
|
||||
) : isLargeMarkdown ? (
|
||||
<LargeMarkdownViewer
|
||||
ref={largeMarkdownRef}
|
||||
content={markdownContent}
|
||||
components={markdownComponents}
|
||||
/>
|
||||
) : (
|
||||
<div className="markdown-body">
|
||||
<ReactMarkdown
|
||||
|
|
@ -255,7 +271,7 @@ function FileSharePage() {
|
|||
rehypePlugins={[rehypeSlug, rehypeHighlight]}
|
||||
components={markdownComponents}
|
||||
>
|
||||
{contentInfo?.content || ''}
|
||||
{markdownContent}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -264,7 +280,7 @@ function FileSharePage() {
|
|||
|
||||
</Content>
|
||||
|
||||
{!isMobile && contentInfo?.type === 'markdown' && (
|
||||
{!isMobile && contentInfo?.type === 'markdown' && !isLargeMarkdown && (
|
||||
<FloatingToc
|
||||
items={tocItems}
|
||||
getContainer={() => contentRef.current}
|
||||
|
|
|
|||
|
|
@ -253,6 +253,13 @@
|
|||
min-height: 100%;
|
||||
}
|
||||
|
||||
.preview-content-wrapper.large-markdown-mode {
|
||||
max-width: 100%;
|
||||
height: calc(100% - 57px);
|
||||
min-height: 0;
|
||||
padding: 0 0 24px;
|
||||
}
|
||||
|
||||
/* PDF模式下使用全宽 */
|
||||
.preview-content-wrapper.pdf-mode {
|
||||
max-width: 100%;
|
||||
|
|
@ -403,6 +410,10 @@
|
|||
padding: 16px;
|
||||
}
|
||||
|
||||
.preview-content-wrapper.large-markdown-mode {
|
||||
padding: 0 0 16px;
|
||||
}
|
||||
|
||||
.preview-content-wrapper.pdf-mode {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
@ -459,6 +470,10 @@
|
|||
padding: 12px;
|
||||
}
|
||||
|
||||
.preview-content-wrapper.large-markdown-mode {
|
||||
padding: 0 0 12px;
|
||||
}
|
||||
|
||||
.preview-content-wrapper.pdf-mode {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import GithubSlugger from 'github-slugger'
|
|||
import Toast from '@/components/Toast/Toast'
|
||||
import FloatingToc, { TocDrawer } from '@/components/FloatingToc/FloatingToc'
|
||||
import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
||||
import LargeMarkdownViewer, { isLargeMarkdownContent } from '@/components/LargeMarkdownViewer/LargeMarkdownViewer'
|
||||
import {
|
||||
getProjectSharePublicInfo,
|
||||
getProjectShareTree,
|
||||
|
|
@ -64,7 +65,9 @@ function ProjectSharePage() {
|
|||
const [isSearching, setIsSearching] = useState(false)
|
||||
const contentRef = useRef(null)
|
||||
const viewerRef = useRef(null)
|
||||
const largeMarkdownRef = useRef(null)
|
||||
const [pdfToolbarTarget, setPdfToolbarTarget] = useState(null)
|
||||
const isLargeMarkdown = isLargeMarkdownContent(markdownContent)
|
||||
|
||||
const handleClose = () => {
|
||||
if (window.history.length > 1) {
|
||||
|
|
@ -82,7 +85,7 @@ function ProjectSharePage() {
|
|||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (viewerRef.current && viewMode === 'markdown') {
|
||||
if (viewerRef.current && viewMode === 'markdown' && !isLargeMarkdown) {
|
||||
const instance = new Mark(viewerRef.current)
|
||||
instance.unmark()
|
||||
if (searchKeyword.trim()) {
|
||||
|
|
@ -93,7 +96,7 @@ function ProjectSharePage() {
|
|||
})
|
||||
}
|
||||
}
|
||||
}, [markdownContent, searchKeyword, viewMode])
|
||||
}, [markdownContent, searchKeyword, viewMode, isLargeMarkdown])
|
||||
|
||||
useEffect(() => {
|
||||
loadProjectInfo()
|
||||
|
|
@ -236,7 +239,10 @@ function ProjectSharePage() {
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!markdownContent) return
|
||||
if (!markdownContent || isLargeMarkdown) {
|
||||
setTocItems([])
|
||||
return
|
||||
}
|
||||
|
||||
const slugger = new GithubSlugger()
|
||||
const headings = []
|
||||
|
|
@ -249,7 +255,7 @@ function ProjectSharePage() {
|
|||
headings.push({ key: `#${key}`, href: `#${key}`, title, level })
|
||||
})
|
||||
setTocItems(headings)
|
||||
}, [markdownContent])
|
||||
}, [markdownContent, isLargeMarkdown])
|
||||
|
||||
const findReadme = (nodes) => {
|
||||
for (const node of nodes) {
|
||||
|
|
@ -383,6 +389,11 @@ function ProjectSharePage() {
|
|||
}
|
||||
|
||||
const scrollContentToTop = () => {
|
||||
if (isLargeMarkdown) {
|
||||
largeMarkdownRef.current?.scrollToTop()
|
||||
return
|
||||
}
|
||||
|
||||
if (contentRef.current) {
|
||||
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
|
@ -549,6 +560,7 @@ function ProjectSharePage() {
|
|||
aria-label="下载PDF"
|
||||
/>
|
||||
</Tooltip>
|
||||
{!isLargeMarkdown && (
|
||||
<Tooltip title="文档索引">
|
||||
<Button
|
||||
icon={<MenuOutlined />}
|
||||
|
|
@ -558,6 +570,7 @@ function ProjectSharePage() {
|
|||
aria-label="文档索引"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
) : (
|
||||
<Space className="preview-header-actions">
|
||||
|
|
@ -580,7 +593,7 @@ function ProjectSharePage() {
|
|||
)}
|
||||
{viewMode === 'pdf' && <div className="preview-header-actions pdf-header-toolbar" ref={setPdfToolbarTarget} />}
|
||||
</div>
|
||||
<div className={`preview-content-wrapper ${viewMode === 'pdf' ? 'pdf-mode' : ''}`}>
|
||||
<div className={`preview-content-wrapper ${viewMode === 'pdf' ? 'pdf-mode' : ''} ${isLargeMarkdown ? 'large-markdown-mode' : ''}`}>
|
||||
{loading ? (
|
||||
<div className="preview-loading">
|
||||
<Spin size="large">
|
||||
|
|
@ -589,6 +602,20 @@ function ProjectSharePage() {
|
|||
</div>
|
||||
) : viewMode === 'pdf' ? (
|
||||
<VirtualPDFViewer url={pdfUrl} filename={pdfFilename} toolbarTarget={pdfToolbarTarget} compactToolbar={isMobile} />
|
||||
) : isLargeMarkdown ? (
|
||||
<LargeMarkdownViewer
|
||||
ref={largeMarkdownRef}
|
||||
content={markdownContent}
|
||||
components={markdownComponents}
|
||||
onClick={(e) => {
|
||||
if (e.defaultPrevented) return
|
||||
const target = e.target.closest('a')
|
||||
if (target) {
|
||||
const href = target.getAttribute('href')
|
||||
if (href) handleMarkdownLink(e, href)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="markdown-body" onClick={(e) => {
|
||||
if (e.defaultPrevented) return
|
||||
|
|
@ -611,7 +638,7 @@ function ProjectSharePage() {
|
|||
|
||||
</Content>
|
||||
|
||||
{!isMobile && viewMode === 'markdown' && (
|
||||
{!isMobile && viewMode === 'markdown' && !isLargeMarkdown && (
|
||||
<FloatingToc
|
||||
items={tocItems}
|
||||
searchKeyword={searchKeyword}
|
||||
|
|
|
|||
Loading…
Reference in New Issue