调整了对超大md文档的支持
parent
9f707e879e
commit
2bb7db0d56
|
|
@ -26,6 +26,7 @@ logs/
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.gemini-clipboard
|
.gemini-clipboard
|
||||||
|
.memsearch
|
||||||
|
|
||||||
# Temporary files
|
# Temporary files
|
||||||
*.tmp
|
*.tmp
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,18 @@
|
||||||
|
|
||||||
## Session 16:54
|
## 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 {
|
.document-editor-page {
|
||||||
height: calc(100vh - 64px);
|
height: calc(100vh - 64px);
|
||||||
/* width: calc(100% + 32px); */
|
/* width: calc(100% + 32px); */
|
||||||
|
|
@ -232,6 +234,15 @@
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bytemd-wrapper.large-file-editor {
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-file-editor-notice {
|
||||||
|
top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Fix for bytemd-react wrapper div */
|
/* Fix for bytemd-react wrapper div */
|
||||||
.bytemd-wrapper>div {
|
.bytemd-wrapper>div {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
@ -242,6 +253,36 @@
|
||||||
min-width: 0;
|
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 {
|
.empty-editor {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import {
|
||||||
FileOutlined,
|
FileOutlined,
|
||||||
FolderOutlined,
|
FolderOutlined,
|
||||||
FolderOpenOutlined,
|
FolderOpenOutlined,
|
||||||
PlusOutlined,
|
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
SaveOutlined,
|
SaveOutlined,
|
||||||
|
|
@ -14,7 +13,6 @@ import {
|
||||||
UploadOutlined,
|
UploadOutlined,
|
||||||
DownloadOutlined,
|
DownloadOutlined,
|
||||||
SwapOutlined,
|
SwapOutlined,
|
||||||
FileImageOutlined,
|
|
||||||
FilePdfOutlined,
|
FilePdfOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
UndoOutlined,
|
UndoOutlined,
|
||||||
|
|
@ -35,11 +33,12 @@ import {
|
||||||
operateFile,
|
operateFile,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
importDocuments,
|
importDocuments,
|
||||||
exportDirectory,
|
|
||||||
uploadDocument,
|
uploadDocument,
|
||||||
|
getDocumentUrl,
|
||||||
} from '@/api/file'
|
} from '@/api/file'
|
||||||
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 { MarkdownSizeNotice, isLargeMarkdownContent } from '@/components/LargeMarkdownViewer/LargeMarkdownViewer'
|
||||||
import './DocumentEditor.css'
|
import './DocumentEditor.css'
|
||||||
|
|
||||||
const { Sider, Content } = Layout
|
const { Sider, Content } = Layout
|
||||||
|
|
@ -63,7 +62,6 @@ function DocumentEditor() {
|
||||||
const [creationParentPath, setCreationParentPath] = useState('')
|
const [creationParentPath, setCreationParentPath] = useState('')
|
||||||
const [moveTargetPath, setMoveTargetPath] = useState('')
|
const [moveTargetPath, setMoveTargetPath] = useState('')
|
||||||
const [dirOptions, setDirOptions] = useState([])
|
const [dirOptions, setDirOptions] = useState([])
|
||||||
const [editorHeight, setEditorHeight] = useState(600) // 设置初始高度为600px
|
|
||||||
const [openKeys, setOpenKeys] = useState([]) // Menu组件的展开项
|
const [openKeys, setOpenKeys] = useState([]) // Menu组件的展开项
|
||||||
const [uploadProgress, setUploadProgress] = useState(0) // 上传进度
|
const [uploadProgress, setUploadProgress] = useState(0) // 上传进度
|
||||||
const [uploading, setUploading] = useState(false) // 是否正在上传
|
const [uploading, setUploading] = useState(false) // 是否正在上传
|
||||||
|
|
@ -78,6 +76,7 @@ function DocumentEditor() {
|
||||||
const [modeSwitchValue, setModeSwitchValue] = useState('edit')
|
const [modeSwitchValue, setModeSwitchValue] = useState('edit')
|
||||||
const editorCtxRef = useRef(null)
|
const editorCtxRef = useRef(null)
|
||||||
const modeSwitchingRef = useRef(false)
|
const modeSwitchingRef = useRef(false)
|
||||||
|
const isLargeMarkdown = isLargeMarkdownContent(fileContent)
|
||||||
|
|
||||||
const isHeaderPdf = selectedFile?.toLowerCase().endsWith('.pdf')
|
const isHeaderPdf = selectedFile?.toLowerCase().endsWith('.pdf')
|
||||||
const HeaderIcon = isHeaderPdf ? FilePdfOutlined : FileTextOutlined
|
const HeaderIcon = isHeaderPdf ? FilePdfOutlined : FileTextOutlined
|
||||||
|
|
@ -109,16 +108,139 @@ function DocumentEditor() {
|
||||||
navigate(to)
|
navigate(to)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFileParam = (filePath) => {
|
const updateSelectedParam = (path, isFile = true) => {
|
||||||
const nextParams = new URLSearchParams(searchParams)
|
const nextParams = new URLSearchParams(searchParams)
|
||||||
if (filePath) {
|
nextParams.delete('file')
|
||||||
nextParams.set('file', filePath)
|
nextParams.delete('selected')
|
||||||
} else {
|
|
||||||
nextParams.delete('file')
|
if (path) {
|
||||||
|
nextParams.set(isFile ? 'file' : 'selected', path)
|
||||||
}
|
}
|
||||||
setSearchParams(nextParams, { replace: true })
|
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) => {
|
const encodeMarkdownLinkTarget = (targetPath) => {
|
||||||
if (!targetPath) return targetPath
|
if (!targetPath) return targetPath
|
||||||
|
|
||||||
|
|
@ -159,22 +281,6 @@ function DocumentEditor() {
|
||||||
setLinkTarget(null)
|
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(() => {
|
useEffect(() => {
|
||||||
fetchTree()
|
fetchTree()
|
||||||
}, [projectId])
|
}, [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) => {
|
const findNodeByKey = (nodes, key) => {
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
|
|
@ -271,8 +351,11 @@ function DocumentEditor() {
|
||||||
setSelectedMenuKey(key)
|
setSelectedMenuKey(key)
|
||||||
|
|
||||||
if (!targetNode.isLeaf) {
|
if (!targetNode.isLeaf) {
|
||||||
|
setSelectedFile(null)
|
||||||
|
setIsPdfSelected(false)
|
||||||
|
setFileContent('')
|
||||||
if (syncUrl) {
|
if (syncUrl) {
|
||||||
updateFileParam(null)
|
updateSelectedParam(key, false)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -306,6 +389,16 @@ function DocumentEditor() {
|
||||||
if (treeData.length === 0) return
|
if (treeData.length === 0) return
|
||||||
|
|
||||||
const fileParam = searchParams.get('file')
|
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) return
|
||||||
if (fileParam === selectedFile) 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) => {
|
const handleMove = (path) => {
|
||||||
setRightClickNode(path)
|
setRightClickNode(path)
|
||||||
|
|
@ -972,8 +1029,7 @@ function DocumentEditor() {
|
||||||
children: node.children ? convertTreeToMenuItems(node.children) : [],
|
children: node.children ? convertTreeToMenuItems(node.children) : [],
|
||||||
className: isSelected ? 'folder-selected' : '',
|
className: isSelected ? 'folder-selected' : '',
|
||||||
onTitleClick: () => {
|
onTitleClick: () => {
|
||||||
setSelectedNode(node)
|
selectFolder(node.key, { syncUrl: true })
|
||||||
setSelectedMenuKey(node.key)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else if (node.title && node.title.endsWith('.md')) {
|
} else if (node.title && node.title.endsWith('.md')) {
|
||||||
|
|
@ -1033,12 +1089,7 @@ function DocumentEditor() {
|
||||||
modeSwitchingRef.current = true
|
modeSwitchingRef.current = true
|
||||||
setModeSwitchValue('view')
|
setModeSwitchValue('view')
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const params = new URLSearchParams()
|
navigateWithTransition(getModeSwitchTarget())
|
||||||
if (selectedFile) {
|
|
||||||
params.set('file', selectedFile)
|
|
||||||
}
|
|
||||||
const query = params.toString()
|
|
||||||
navigateWithTransition(`/projects/${projectId}/docs${query ? `?${query}` : ''}`)
|
|
||||||
}, 160)
|
}, 160)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
@ -1073,11 +1124,11 @@ function DocumentEditor() {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Upload>
|
</Upload>
|
||||||
<Tooltip title="导出文档">
|
<Tooltip title="下载选中文件">
|
||||||
<Button
|
<Button
|
||||||
size="middle"
|
size="middle"
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined />}
|
||||||
onClick={handleExportDirectory}
|
onClick={handleDownloadSelectedFile}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
|
|
@ -1135,23 +1186,35 @@ function DocumentEditor() {
|
||||||
</div>
|
</div>
|
||||||
) : selectedFile ? (
|
) : selectedFile ? (
|
||||||
<div
|
<div
|
||||||
className="bytemd-wrapper"
|
className={`bytemd-wrapper ${isLargeMarkdown ? 'large-file-editor' : ''}`}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onDragOver={(e) => e.preventDefault()}
|
onDragOver={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<Editor
|
{isLargeMarkdown ? (
|
||||||
key={selectedFile}
|
<>
|
||||||
value={fileContent}
|
<MarkdownSizeNotice className="large-file-editor-notice" />
|
||||||
onChange={(v) => setFileContent(v)}
|
<textarea
|
||||||
plugins={plugins}
|
className="large-markdown-textarea"
|
||||||
locale={{
|
value={fileContent}
|
||||||
en: {
|
onChange={(e) => setFileContent(e.target.value)}
|
||||||
'Write': '编辑',
|
spellCheck={false}
|
||||||
'Preview': '预览',
|
/>
|
||||||
},
|
</>
|
||||||
}}
|
) : (
|
||||||
/>
|
<Editor
|
||||||
|
key={selectedFile}
|
||||||
|
value={fileContent}
|
||||||
|
onChange={(v) => setFileContent(v)}
|
||||||
|
plugins={plugins}
|
||||||
|
locale={{
|
||||||
|
en: {
|
||||||
|
'Write': '编辑',
|
||||||
|
'Preview': '预览',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="empty-editor">
|
<div className="empty-editor">
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,13 @@
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.docs-content-wrapper.large-markdown-mode {
|
||||||
|
max-width: 100%;
|
||||||
|
height: calc(100% - 57px);
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
/* PDF模式下使用全宽 */
|
/* PDF模式下使用全宽 */
|
||||||
.docs-content-wrapper.pdf-mode {
|
.docs-content-wrapper.pdf-mode {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
@ -260,6 +267,16 @@
|
||||||
min-height: 400px;
|
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 {
|
.markdown-body {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
|
|
||||||
|
|
@ -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, Button, Tooltip, message, Modal, Input, Space, Dropdown, Empty, Switch } from 'antd'
|
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 ReactMarkdown from 'react-markdown'
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
import rehypeRaw from 'rehype-raw'
|
import rehypeRaw from 'rehype-raw'
|
||||||
|
|
@ -18,9 +18,11 @@ import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
||||||
import FloatingToc from '@/components/FloatingToc/FloatingToc'
|
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 LargeMarkdownViewer, { isLargeMarkdownContent } from '@/components/LargeMarkdownViewer/LargeMarkdownViewer'
|
||||||
import './DocumentPage.css'
|
import './DocumentPage.css'
|
||||||
|
|
||||||
const { Sider, Content } = Layout
|
const { Sider, Content } = Layout
|
||||||
|
const MAX_TOC_ITEMS = 500
|
||||||
|
|
||||||
// 高亮渲染组件
|
// 高亮渲染组件
|
||||||
const HighlightText = ({ text, keyword }) => {
|
const HighlightText = ({ text, keyword }) => {
|
||||||
|
|
@ -51,7 +53,6 @@ function DocumentPage() {
|
||||||
const [hasPassword, setHasPassword] = useState(false)
|
const [hasPassword, setHasPassword] = useState(false)
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [userRole, setUserRole] = useState('viewer')
|
const [userRole, setUserRole] = useState('viewer')
|
||||||
const [pdfViewerVisible, setPdfViewerVisible] = useState(false)
|
|
||||||
const [pdfUrl, setPdfUrl] = useState('')
|
const [pdfUrl, setPdfUrl] = useState('')
|
||||||
const [pdfFilename, setPdfFilename] = useState('')
|
const [pdfFilename, setPdfFilename] = useState('')
|
||||||
const [viewMode, setViewMode] = useState('markdown')
|
const [viewMode, setViewMode] = useState('markdown')
|
||||||
|
|
@ -66,14 +67,18 @@ function DocumentPage() {
|
||||||
const [modeSwitchValue, setModeSwitchValue] = useState('view')
|
const [modeSwitchValue, setModeSwitchValue] = useState('view')
|
||||||
|
|
||||||
const contentRef = useRef(null)
|
const contentRef = useRef(null)
|
||||||
|
const largeMarkdownRef = useRef(null)
|
||||||
const modeSwitchingRef = useRef(false)
|
const modeSwitchingRef = useRef(false)
|
||||||
const [pdfToolbarTarget, setPdfToolbarTarget] = useState(null)
|
const [pdfToolbarTarget, setPdfToolbarTarget] = useState(null)
|
||||||
|
const isLargeMarkdown = isLargeMarkdownContent(markdownContent)
|
||||||
|
|
||||||
const getHeaderDisplay = (filePath) => {
|
const getHeaderDisplay = (filePath) => {
|
||||||
const resolvedPath = filePath || 'README.md'
|
const resolvedPath = selectedNodeKey || filePath || 'README.md'
|
||||||
const fileName = resolvedPath.split('/').filter(Boolean).pop() || '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 isPdf = fileName.toLowerCase().endsWith('.pdf')
|
||||||
const FileIcon = isPdf ? FilePdfOutlined : FileTextOutlined
|
const FileIcon = isFolder ? FolderOutlined : isPdf ? FilePdfOutlined : FileTextOutlined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fileName,
|
fileName,
|
||||||
|
|
@ -90,16 +95,68 @@ function DocumentPage() {
|
||||||
navigate(to)
|
navigate(to)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFileParam = (filePath) => {
|
const updateSelectedParam = (path, isFile = true) => {
|
||||||
const nextParams = new URLSearchParams(searchParams)
|
const nextParams = new URLSearchParams(searchParams)
|
||||||
if (filePath) {
|
nextParams.delete('file')
|
||||||
nextParams.set('file', filePath)
|
nextParams.delete('selected')
|
||||||
} else {
|
|
||||||
nextParams.delete('file')
|
if (path) {
|
||||||
|
nextParams.set(isFile ? 'file' : 'selected', path)
|
||||||
}
|
}
|
||||||
setSearchParams(nextParams, { replace: true })
|
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 buildDocumentUrl = (filePath, refreshKey = null) => {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
const token = localStorage.getItem('access_token')
|
const token = localStorage.getItem('access_token')
|
||||||
|
|
@ -141,6 +198,7 @@ function DocumentPage() {
|
||||||
if (fileTree.length === 0) return
|
if (fileTree.length === 0) return
|
||||||
|
|
||||||
const fileParam = searchParams.get('file')
|
const fileParam = searchParams.get('file')
|
||||||
|
const selectedParam = searchParams.get('selected')
|
||||||
const keywordParam = searchParams.get('keyword')
|
const keywordParam = searchParams.get('keyword')
|
||||||
|
|
||||||
// 处理搜索
|
// 处理搜索
|
||||||
|
|
@ -149,42 +207,24 @@ function DocumentPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理文件加载
|
// 处理文件加载
|
||||||
if (fileParam) {
|
if (selectedParam) {
|
||||||
if (fileParam !== selectedFile) {
|
const targetNode = findNodeByKey(fileTree, selectedParam)
|
||||||
setSelectedFile(fileParam)
|
if (targetNode && !targetNode.isLeaf && selectedParam !== selectedNodeKey) {
|
||||||
setSelectedNodeKey(fileParam)
|
selectFolder(selectedParam)
|
||||||
|
}
|
||||||
// 展开父目录
|
} else if (fileParam) {
|
||||||
const parts = fileParam.split('/')
|
const targetNode = findNodeByKey(fileTree, fileParam)
|
||||||
const allParentPaths = []
|
if (targetNode && !targetNode.isLeaf) {
|
||||||
let currentPath = ''
|
selectFolder(fileParam, { syncUrl: true })
|
||||||
for (let i = 0; i < parts.length - 1; i++) {
|
} else if (fileParam !== selectedFile) {
|
||||||
currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i]
|
openDocumentPath(fileParam)
|
||||||
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')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 如果没有指定文件,且当前没有选中文件,默认打开 README.md
|
// 如果没有指定文件,且当前没有选中文件,默认打开 README.md
|
||||||
if (!selectedFile) {
|
if (!selectedFile && !selectedNodeKey) {
|
||||||
const readmeNode = findReadme(fileTree)
|
const readmeNode = findReadme(fileTree)
|
||||||
if (readmeNode) {
|
if (readmeNode) {
|
||||||
setSelectedFile(readmeNode.key)
|
openDocumentPath(readmeNode.key, { syncUrl: true })
|
||||||
setSelectedNodeKey(readmeNode.key)
|
|
||||||
updateFileParam(readmeNode.key)
|
|
||||||
loadMarkdown(readmeNode.key)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -325,7 +365,7 @@ function DocumentPage() {
|
||||||
key: node.key,
|
key: node.key,
|
||||||
label: labelNode,
|
label: labelNode,
|
||||||
icon: isOpen ? <FolderOpenOutlined /> : <FolderOutlined />,
|
icon: isOpen ? <FolderOpenOutlined /> : <FolderOutlined />,
|
||||||
onTitleClick: () => setSelectedNodeKey(node.key),
|
onTitleClick: () => selectFolder(node.key, { syncUrl: true }),
|
||||||
children: node.children ? convertTreeToMenuItems(node.children) : [],
|
children: node.children ? convertTreeToMenuItems(node.children) : [],
|
||||||
}
|
}
|
||||||
} else if (node.title && node.title.endsWith('.md')) {
|
} else if (node.title && node.title.endsWith('.md')) {
|
||||||
|
|
@ -357,7 +397,7 @@ function DocumentPage() {
|
||||||
|
|
||||||
// 滚动到顶部
|
// 滚动到顶部
|
||||||
if (contentRef.current) {
|
if (contentRef.current) {
|
||||||
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' })
|
contentRef.current.scrollTo({ top: 0, behavior: 'auto' })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Load markdown error:', error)
|
console.error('Load markdown error:', error)
|
||||||
|
|
@ -369,12 +409,19 @@ function DocumentPage() {
|
||||||
|
|
||||||
// 提取 markdown 标题生成目录
|
// 提取 markdown 标题生成目录
|
||||||
useEffect(() => {
|
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 slugger = new GithubSlugger()
|
||||||
const headings = []
|
const headings = []
|
||||||
const lines = markdownContent.split('\n')
|
const lines = markdownContent.split('\n')
|
||||||
|
|
||||||
lines.forEach((line) => {
|
for (const line of lines) {
|
||||||
const match = line.match(/^(#{1,6})\s+(.+)$/)
|
const match = line.match(/^(#{1,6})\s+(.+)$/)
|
||||||
if (match) {
|
if (match) {
|
||||||
const level = match[1].length
|
const level = match[1].length
|
||||||
|
|
@ -388,33 +435,44 @@ function DocumentPage() {
|
||||||
title,
|
title,
|
||||||
level,
|
level,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (headings.length >= MAX_TOC_ITEMS) {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
setTocItems(headings)
|
setTocItems(headings)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
canceled = true
|
||||||
|
cancel(taskId)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setTocItems([])
|
||||||
}
|
}
|
||||||
}, [markdownContent])
|
}, [markdownContent, isLargeMarkdown])
|
||||||
|
|
||||||
// 处理菜单点击
|
// 处理菜单点击
|
||||||
const handleMenuClick = ({ key }) => {
|
const handleMenuClick = ({ key }) => {
|
||||||
setSelectedFile(key)
|
const node = findNodeByKey(fileTree, key)
|
||||||
setSelectedNodeKey(key)
|
if (!node) return
|
||||||
updateFileParam(key)
|
|
||||||
|
|
||||||
// 检查是否是PDF文件
|
if (!node.isLeaf) {
|
||||||
if (key.toLowerCase().endsWith('.pdf')) {
|
selectFolder(key, { syncUrl: true })
|
||||||
// 显示PDF - 添加token到URL
|
return
|
||||||
setPdfUrl(buildDocumentUrl(key))
|
|
||||||
setPdfFilename(key.split('/').pop())
|
|
||||||
setViewMode('pdf')
|
|
||||||
} else {
|
|
||||||
// 加载Markdown文件
|
|
||||||
setViewMode('markdown')
|
|
||||||
loadMarkdown(key)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openDocumentPath(key, { syncUrl: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollContentToTop = () => {
|
const scrollContentToTop = () => {
|
||||||
|
if (isLargeMarkdown) {
|
||||||
|
largeMarkdownRef.current?.scrollToTop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (contentRef.current) {
|
if (contentRef.current) {
|
||||||
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' })
|
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
}
|
}
|
||||||
|
|
@ -712,6 +770,8 @@ function DocumentPage() {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
params.set('file', selectedFile)
|
params.set('file', selectedFile)
|
||||||
|
} else if (selectedNodeKey) {
|
||||||
|
params.set('selected', selectedNodeKey)
|
||||||
}
|
}
|
||||||
const query = params.toString()
|
const query = params.toString()
|
||||||
navigateWithTransition(`/projects/${projectId}/editor${query ? `?${query}` : ''}`)
|
navigateWithTransition(`/projects/${projectId}/editor${query ? `?${query}` : ''}`)
|
||||||
|
|
@ -1077,7 +1137,7 @@ function DocumentPage() {
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<div className={`docs-content-wrapper ${viewMode === 'pdf' ? 'pdf-mode' : ''}`}>
|
<div className={`docs-content-wrapper ${viewMode === 'pdf' ? 'pdf-mode' : ''} ${isLargeMarkdown ? 'large-markdown-mode' : ''}`}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="docs-loading">
|
<div className="docs-loading">
|
||||||
<Spin size="large">
|
<Spin size="large">
|
||||||
|
|
@ -1090,6 +1150,17 @@ function DocumentPage() {
|
||||||
filename={pdfFilename}
|
filename={pdfFilename}
|
||||||
toolbarTarget={pdfToolbarTarget}
|
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">
|
<div className="markdown-body">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
|
|
@ -1105,7 +1176,7 @@ function DocumentPage() {
|
||||||
|
|
||||||
</Content>
|
</Content>
|
||||||
|
|
||||||
{viewMode === 'markdown' && (
|
{viewMode === 'markdown' && !isLargeMarkdown && (
|
||||||
<FloatingToc
|
<FloatingToc
|
||||||
items={tocItems}
|
items={tocItems}
|
||||||
searchKeyword={searchKeyword}
|
searchKeyword={searchKeyword}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import GithubSlugger from 'github-slugger'
|
||||||
import Toast from '@/components/Toast/Toast'
|
import Toast from '@/components/Toast/Toast'
|
||||||
import FloatingToc, { TocDrawer } from '@/components/FloatingToc/FloatingToc'
|
import FloatingToc, { TocDrawer } from '@/components/FloatingToc/FloatingToc'
|
||||||
import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
||||||
|
import LargeMarkdownViewer, { isLargeMarkdownContent } from '@/components/LargeMarkdownViewer/LargeMarkdownViewer'
|
||||||
import {
|
import {
|
||||||
getFileSharePublicInfo,
|
getFileSharePublicInfo,
|
||||||
verifyFileSharePassword,
|
verifyFileSharePassword,
|
||||||
|
|
@ -25,6 +26,7 @@ function FileSharePage() {
|
||||||
const { shareCode } = useParams()
|
const { shareCode } = useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const contentRef = useRef(null)
|
const contentRef = useRef(null)
|
||||||
|
const largeMarkdownRef = useRef(null)
|
||||||
const [pdfToolbarTarget, setPdfToolbarTarget] = useState(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)
|
||||||
|
|
@ -34,6 +36,8 @@ function FileSharePage() {
|
||||||
const [tocDrawerVisible, setTocDrawerVisible] = useState(false)
|
const [tocDrawerVisible, setTocDrawerVisible] = useState(false)
|
||||||
const [passwordModalVisible, setPasswordModalVisible] = useState(false)
|
const [passwordModalVisible, setPasswordModalVisible] = useState(false)
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
|
const markdownContent = contentInfo?.type === 'markdown' ? (contentInfo.content || '') : ''
|
||||||
|
const isLargeMarkdown = isLargeMarkdownContent(markdownContent)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFileShare()
|
loadFileShare()
|
||||||
|
|
@ -47,8 +51,7 @@ function FileSharePage() {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const markdownContent = contentInfo?.type === 'markdown' ? (contentInfo.content || '') : ''
|
if (!markdownContent || isLargeMarkdown) {
|
||||||
if (!markdownContent) {
|
|
||||||
setTocItems([])
|
setTocItems([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -64,7 +67,7 @@ function FileSharePage() {
|
||||||
headings.push({ key: `#${key}`, href: `#${key}`, title, level })
|
headings.push({ key: `#${key}`, href: `#${key}`, title, level })
|
||||||
})
|
})
|
||||||
setTocItems(headings)
|
setTocItems(headings)
|
||||||
}, [contentInfo])
|
}, [markdownContent, isLargeMarkdown])
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (window.history.length > 1) {
|
if (window.history.length > 1) {
|
||||||
|
|
@ -119,6 +122,11 @@ function FileSharePage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollContentToTop = () => {
|
const scrollContentToTop = () => {
|
||||||
|
if (isLargeMarkdown) {
|
||||||
|
largeMarkdownRef.current?.scrollToTop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (contentRef.current) {
|
if (contentRef.current) {
|
||||||
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' })
|
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
}
|
}
|
||||||
|
|
@ -202,15 +210,17 @@ function FileSharePage() {
|
||||||
aria-label="下载PDF"
|
aria-label="下载PDF"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="文档索引">
|
{!isLargeMarkdown && (
|
||||||
<Button
|
<Tooltip title="文档索引">
|
||||||
icon={<MenuOutlined />}
|
<Button
|
||||||
onClick={() => setTocDrawerVisible(true)}
|
icon={<MenuOutlined />}
|
||||||
size="small"
|
onClick={() => setTocDrawerVisible(true)}
|
||||||
type="text"
|
size="small"
|
||||||
aria-label="文档索引"
|
type="text"
|
||||||
/>
|
aria-label="文档索引"
|
||||||
</Tooltip>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
) : (
|
) : (
|
||||||
<Space className="preview-header-actions">
|
<Space className="preview-header-actions">
|
||||||
|
|
@ -240,7 +250,7 @@ function FileSharePage() {
|
||||||
</Spin>
|
</Spin>
|
||||||
</div>
|
</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' ? (
|
{contentInfo?.type === 'pdf' ? (
|
||||||
<VirtualPDFViewer
|
<VirtualPDFViewer
|
||||||
url={contentInfo.document_url}
|
url={contentInfo.document_url}
|
||||||
|
|
@ -248,6 +258,12 @@ function FileSharePage() {
|
||||||
toolbarTarget={pdfToolbarTarget}
|
toolbarTarget={pdfToolbarTarget}
|
||||||
compactToolbar={isMobile}
|
compactToolbar={isMobile}
|
||||||
/>
|
/>
|
||||||
|
) : isLargeMarkdown ? (
|
||||||
|
<LargeMarkdownViewer
|
||||||
|
ref={largeMarkdownRef}
|
||||||
|
content={markdownContent}
|
||||||
|
components={markdownComponents}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="markdown-body">
|
<div className="markdown-body">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
|
|
@ -255,7 +271,7 @@ function FileSharePage() {
|
||||||
rehypePlugins={[rehypeSlug, rehypeHighlight]}
|
rehypePlugins={[rehypeSlug, rehypeHighlight]}
|
||||||
components={markdownComponents}
|
components={markdownComponents}
|
||||||
>
|
>
|
||||||
{contentInfo?.content || ''}
|
{markdownContent}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -264,7 +280,7 @@ function FileSharePage() {
|
||||||
|
|
||||||
</Content>
|
</Content>
|
||||||
|
|
||||||
{!isMobile && contentInfo?.type === 'markdown' && (
|
{!isMobile && contentInfo?.type === 'markdown' && !isLargeMarkdown && (
|
||||||
<FloatingToc
|
<FloatingToc
|
||||||
items={tocItems}
|
items={tocItems}
|
||||||
getContainer={() => contentRef.current}
|
getContainer={() => contentRef.current}
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,13 @@
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-content-wrapper.large-markdown-mode {
|
||||||
|
max-width: 100%;
|
||||||
|
height: calc(100% - 57px);
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
/* PDF模式下使用全宽 */
|
/* PDF模式下使用全宽 */
|
||||||
.preview-content-wrapper.pdf-mode {
|
.preview-content-wrapper.pdf-mode {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
@ -403,6 +410,10 @@
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-content-wrapper.large-markdown-mode {
|
||||||
|
padding: 0 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.preview-content-wrapper.pdf-mode {
|
.preview-content-wrapper.pdf-mode {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -459,6 +470,10 @@
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-content-wrapper.large-markdown-mode {
|
||||||
|
padding: 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.preview-content-wrapper.pdf-mode {
|
.preview-content-wrapper.pdf-mode {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import GithubSlugger from 'github-slugger'
|
||||||
import Toast from '@/components/Toast/Toast'
|
import Toast from '@/components/Toast/Toast'
|
||||||
import FloatingToc, { TocDrawer } from '@/components/FloatingToc/FloatingToc'
|
import FloatingToc, { TocDrawer } from '@/components/FloatingToc/FloatingToc'
|
||||||
import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
||||||
|
import LargeMarkdownViewer, { isLargeMarkdownContent } from '@/components/LargeMarkdownViewer/LargeMarkdownViewer'
|
||||||
import {
|
import {
|
||||||
getProjectSharePublicInfo,
|
getProjectSharePublicInfo,
|
||||||
getProjectShareTree,
|
getProjectShareTree,
|
||||||
|
|
@ -64,7 +65,9 @@ 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 largeMarkdownRef = useRef(null)
|
||||||
const [pdfToolbarTarget, setPdfToolbarTarget] = useState(null)
|
const [pdfToolbarTarget, setPdfToolbarTarget] = useState(null)
|
||||||
|
const isLargeMarkdown = isLargeMarkdownContent(markdownContent)
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (window.history.length > 1) {
|
if (window.history.length > 1) {
|
||||||
|
|
@ -82,7 +85,7 @@ function ProjectSharePage() {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (viewerRef.current && viewMode === 'markdown') {
|
if (viewerRef.current && viewMode === 'markdown' && !isLargeMarkdown) {
|
||||||
const instance = new Mark(viewerRef.current)
|
const instance = new Mark(viewerRef.current)
|
||||||
instance.unmark()
|
instance.unmark()
|
||||||
if (searchKeyword.trim()) {
|
if (searchKeyword.trim()) {
|
||||||
|
|
@ -93,7 +96,7 @@ function ProjectSharePage() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [markdownContent, searchKeyword, viewMode])
|
}, [markdownContent, searchKeyword, viewMode, isLargeMarkdown])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadProjectInfo()
|
loadProjectInfo()
|
||||||
|
|
@ -236,7 +239,10 @@ function ProjectSharePage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!markdownContent) return
|
if (!markdownContent || isLargeMarkdown) {
|
||||||
|
setTocItems([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const slugger = new GithubSlugger()
|
const slugger = new GithubSlugger()
|
||||||
const headings = []
|
const headings = []
|
||||||
|
|
@ -249,7 +255,7 @@ function ProjectSharePage() {
|
||||||
headings.push({ key: `#${key}`, href: `#${key}`, title, level })
|
headings.push({ key: `#${key}`, href: `#${key}`, title, level })
|
||||||
})
|
})
|
||||||
setTocItems(headings)
|
setTocItems(headings)
|
||||||
}, [markdownContent])
|
}, [markdownContent, isLargeMarkdown])
|
||||||
|
|
||||||
const findReadme = (nodes) => {
|
const findReadme = (nodes) => {
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
|
|
@ -383,6 +389,11 @@ function ProjectSharePage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollContentToTop = () => {
|
const scrollContentToTop = () => {
|
||||||
|
if (isLargeMarkdown) {
|
||||||
|
largeMarkdownRef.current?.scrollToTop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (contentRef.current) {
|
if (contentRef.current) {
|
||||||
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' })
|
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
}
|
}
|
||||||
|
|
@ -549,15 +560,17 @@ function ProjectSharePage() {
|
||||||
aria-label="下载PDF"
|
aria-label="下载PDF"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="文档索引">
|
{!isLargeMarkdown && (
|
||||||
<Button
|
<Tooltip title="文档索引">
|
||||||
icon={<MenuOutlined />}
|
<Button
|
||||||
onClick={() => setTocDrawerVisible(true)}
|
icon={<MenuOutlined />}
|
||||||
size="small"
|
onClick={() => setTocDrawerVisible(true)}
|
||||||
type="text"
|
size="small"
|
||||||
aria-label="文档索引"
|
type="text"
|
||||||
/>
|
aria-label="文档索引"
|
||||||
</Tooltip>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
) : (
|
) : (
|
||||||
<Space className="preview-header-actions">
|
<Space className="preview-header-actions">
|
||||||
|
|
@ -580,7 +593,7 @@ function ProjectSharePage() {
|
||||||
)}
|
)}
|
||||||
{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>
|
||||||
<div className={`preview-content-wrapper ${viewMode === 'pdf' ? 'pdf-mode' : ''}`}>
|
<div className={`preview-content-wrapper ${viewMode === 'pdf' ? 'pdf-mode' : ''} ${isLargeMarkdown ? 'large-markdown-mode' : ''}`}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="preview-loading">
|
<div className="preview-loading">
|
||||||
<Spin size="large">
|
<Spin size="large">
|
||||||
|
|
@ -589,6 +602,20 @@ function ProjectSharePage() {
|
||||||
</div>
|
</div>
|
||||||
) : viewMode === 'pdf' ? (
|
) : viewMode === 'pdf' ? (
|
||||||
<VirtualPDFViewer url={pdfUrl} filename={pdfFilename} toolbarTarget={pdfToolbarTarget} compactToolbar={isMobile} />
|
<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) => {
|
<div className="markdown-body" onClick={(e) => {
|
||||||
if (e.defaultPrevented) return
|
if (e.defaultPrevented) return
|
||||||
|
|
@ -611,7 +638,7 @@ function ProjectSharePage() {
|
||||||
|
|
||||||
</Content>
|
</Content>
|
||||||
|
|
||||||
{!isMobile && viewMode === 'markdown' && (
|
{!isMobile && viewMode === 'markdown' && !isLargeMarkdown && (
|
||||||
<FloatingToc
|
<FloatingToc
|
||||||
items={tocItems}
|
items={tocItems}
|
||||||
searchKeyword={searchKeyword}
|
searchKeyword={searchKeyword}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue