import { useState, useEffect, useRef } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { Layout, Menu, Spin, FloatButton, Button, Tooltip, message, Anchor, Modal, Input, Switch, Space } from 'antd' import { EditOutlined, VerticalAlignTopOutlined, ShareAltOutlined, SettingOutlined, MenuFoldOutlined, MenuUnfoldOutlined, FileTextOutlined, FolderOutlined, FilePdfOutlined, CopyOutlined, LockOutlined } from '@ant-design/icons' 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' import 'highlight.js/styles/github.css' import { getProjectTree, getFileContent, getDocumentUrl } from '@/api/file' import { getProjectShareInfo, updateShareSettings } from '@/api/share' import PDFViewer from '@/components/PDFViewer/PDFViewer' import './DocumentPage.css' const { Sider, Content } = Layout function DocumentPage() { const { projectId } = useParams() const navigate = useNavigate() const [fileTree, setFileTree] = useState([]) const [selectedFile, setSelectedFile] = useState('') const [markdownContent, setMarkdownContent] = useState('') const [loading, setLoading] = useState(false) const [openKeys, setOpenKeys] = useState([]) const [tocCollapsed, setTocCollapsed] = useState(false) const [tocItems, setTocItems] = useState([]) const [shareModalVisible, setShareModalVisible] = useState(false) const [shareInfo, setShareInfo] = useState(null) const [hasPassword, setHasPassword] = useState(false) const [password, setPassword] = useState('') const [userRole, setUserRole] = useState('viewer') // 用户角色:owner/admin/editor/viewer const [pdfViewerVisible, setPdfViewerVisible] = useState(false) const [pdfUrl, setPdfUrl] = useState('') const [pdfFilename, setPdfFilename] = useState('') const [viewMode, setViewMode] = useState('markdown') // 'markdown' or 'pdf' const contentRef = useRef(null) useEffect(() => { loadFileTree() }, [projectId]) // 加载文件树 const loadFileTree = async () => { try { const res = await getProjectTree(projectId) const data = res.data || {} const tree = data.tree || data || [] // 兼容新旧格式 const role = data.user_role || 'viewer' setFileTree(tree) setUserRole(role) // 默认打开 README.md const readmeNode = findReadme(tree) if (readmeNode) { setSelectedFile(readmeNode.key) loadMarkdown(readmeNode.key) } } catch (error) { console.error('Load file tree error:', error) } } // 查找根目录的 README.md const findReadme = (nodes) => { // 只在根目录查找 for (const node of nodes) { if (node.title === 'README.md' && node.isLeaf) { return node } } return null } // 转换文件树为菜单项 const convertTreeToMenuItems = (nodes) => { return nodes.map((node) => { if (!node.isLeaf) { // 目录 return { key: node.key, label: node.title, icon: , children: node.children ? convertTreeToMenuItems(node.children) : [], } } else if (node.title && node.title.endsWith('.md')) { // Markdown 文件 return { key: node.key, label: node.title.replace('.md', ''), icon: , } } else if (node.title && node.title.endsWith('.pdf')) { // PDF 文件 return { key: node.key, label: node.title, icon: , } } return null }).filter(Boolean) } // 加载 markdown 文件 const loadMarkdown = async (filePath) => { setLoading(true) setTocItems([]) // 清空旧的目录数据 try { const res = await getFileContent(projectId, filePath) setMarkdownContent(res.data?.content || '') // 滚动到顶部 if (contentRef.current) { contentRef.current.scrollTo({ top: 0, behavior: 'smooth' }) } } catch (error) { console.error('Load markdown error:', error) setMarkdownContent('# 文档加载失败\n\n无法加载该文档,请稍后重试。') } finally { setLoading(false) } } // 提取 markdown 标题生成目录 useEffect(() => { if (markdownContent) { const headings = [] const lines = markdownContent.split('\n') lines.forEach((line) => { const match = line.match(/^(#{1,6})\s+(.+)$/) if (match) { const level = match[1].length const title = match[2] const key = title.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-') headings.push({ key: `#${key}`, href: `#${key}`, title, level, }) } }) setTocItems(headings) } }, [markdownContent]) // 处理菜单点击 const handleMenuClick = ({ key }) => { setSelectedFile(key) // 检查是否是PDF文件 if (key.toLowerCase().endsWith('.pdf')) { // 显示PDF - 添加token到URL let url = getDocumentUrl(projectId, key) const token = localStorage.getItem('access_token') if (token) { url += `?token=${encodeURIComponent(token)}` } setPdfUrl(url) setPdfFilename(key.split('/').pop()) setViewMode('pdf') } else { // 加载Markdown文件 setViewMode('markdown') loadMarkdown(key) } } // 解析相对路径 const resolveRelativePath = (currentPath, relativePath) => { // 获取当前文件所在目录 const currentDir = currentPath.substring(0, currentPath.lastIndexOf('/')) // 分割相对路径 const parts = relativePath.split('/') const dirParts = currentDir ? currentDir.split('/') : [] // 处理 ../ 和 ./ for (const part of parts) { if (part === '..') { dirParts.pop() } else if (part !== '.' && part !== '') { dirParts.push(part) } } return dirParts.join('/') } // 处理markdown内部链接点击 const handleMarkdownLink = (e, href) => { // 检查是否是外部链接 if (!href || href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//')) { return // 外部链接,允许默认行为 } // 检查是否是锚点链接 if (href.startsWith('#')) { return // 锚点链接,允许默认行为 } // 检查是否是文档文件(.md 或 .pdf) const isMd = href.endsWith('.md') const isPdf = href.toLowerCase().endsWith('.pdf') if (!isMd && !isPdf) { return // 不是文档文件,允许默认行为 } // 阻止默认跳转 e.preventDefault() // 先解码 href(因为 Markdown 中的链接可能已经是 URL 编码的) let decodedHref = href try { decodedHref = decodeURIComponent(href) } catch (e) { // 解码失败,使用原始值 } // 解析相对路径 const targetPath = resolveRelativePath(selectedFile, decodedHref) // 自动展开父目录 const parentPath = targetPath.substring(0, targetPath.lastIndexOf('/')) if (parentPath && !openKeys.includes(parentPath)) { // 收集所有父路径 const pathParts = parentPath.split('/') const allParentPaths = [] let currentPath = '' for (const part of pathParts) { currentPath = currentPath ? `${currentPath}/${part}` : part allParentPaths.push(currentPath) } setOpenKeys([...new Set([...openKeys, ...allParentPaths])]) } // 选中文件并加载 setSelectedFile(targetPath) if (isPdf) { // PDF文件:切换到PDF模式 let url = getDocumentUrl(projectId, targetPath) const token = localStorage.getItem('access_token') if (token) { url += `?token=${encodeURIComponent(token)}` } setPdfUrl(url) setPdfFilename(targetPath.split('/').pop()) setViewMode('pdf') } else { // Markdown文件:加载内容 setViewMode('markdown') loadMarkdown(targetPath) } } // 进入编辑模式 const handleEdit = () => { navigate(`/projects/${projectId}/editor`) } // 打开分享设置 const handleShare = async () => { try { const res = await getProjectShareInfo(projectId) setShareInfo(res.data) setHasPassword(res.data.has_password) setPassword(res.data.access_pass || '') // 显示已设置的密码 setShareModalVisible(true) } catch (error) { console.error('Get share info error:', error) message.error('获取分享信息失败') } } // 复制分享链接 const handleCopyLink = () => { if (!shareInfo) return const fullUrl = `${window.location.origin}${shareInfo.share_url}` navigator.clipboard.writeText(fullUrl) message.success('分享链接已复制') } // 切换密码保护 const handlePasswordToggle = async (checked) => { if (!checked) { // 取消密码 try { await updateShareSettings(projectId, { access_pass: null }) setHasPassword(false) setPassword('') message.success('已取消访问密码') // 刷新分享信息 const res = await getProjectShareInfo(projectId) setShareInfo(res.data) } catch (error) { console.error('Update settings error:', error) message.error('操作失败') } } else { setHasPassword(true) } } // 保存密码 const handleSavePassword = async () => { if (!password.trim()) { message.warning('请输入访问密码') return } try { await updateShareSettings(projectId, { access_pass: password }) message.success('访问密码已设置') // 刷新分享信息 const res = await getProjectShareInfo(projectId) setShareInfo(res.data) setHasPassword(true) } catch (error) { console.error('Save password error:', error) message.error('设置密码失败') } } const menuItems = convertTreeToMenuItems(fileTree) return (
{/* 左侧目录 */}

项目文档(浏览模式)

{/* 只有 owner/admin/editor 可以编辑 */} {userRole !== 'viewer' && (
{/* 右侧内容区 */}
{loading ? (
加载中...
) : viewMode === 'pdf' ? ( ) : ( )}
{/* 返回顶部按钮 - 仅在markdown模式显示 */} {viewMode === 'markdown' && ( } type="primary" style={{ right: tocCollapsed ? 24 : 280 }} onClick={() => { if (contentRef.current) { contentRef.current.scrollTo({ top: 0, behavior: 'smooth' }) } }} /> )}
{/* 右侧TOC面板 - 仅在markdown模式显示 */} {viewMode === 'markdown' && !tocCollapsed && (

文档索引

{tocItems.length > 0 ? ( contentRef.current} items={tocItems.map((item) => ({ key: item.key, href: item.href, title: (
{item.title}
), }))} /> ) : (
当前文档无标题
)}
)}
{/* TOC展开按钮 */} {tocCollapsed && ( )} {/* 分享模态框 */} setShareModalVisible(false)} footer={null} width={500} > {shareInfo && (
} />
访问密码保护
{hasPassword && (
setPassword(e.target.value)} />
)}
)}
) } export default DocumentPage