import { useState, useEffect, useRef } from 'react' import { useParams } from 'react-router-dom' import { Layout, Menu, Spin, FloatButton, Button, Modal, Input, message, Drawer, Anchor } from 'antd' import { VerticalAlignTopOutlined, MenuOutlined, MenuFoldOutlined, MenuUnfoldOutlined, FileTextOutlined, FolderOutlined, FilePdfOutlined, 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 { getPreviewInfo, getPreviewTree, getPreviewFile, verifyAccessPassword, getPreviewDocumentUrl } from '@/api/share' import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer' import './PreviewPage.css' const { Sider, Content } = Layout function PreviewPage() { const { projectId } = useParams() const [projectInfo, setProjectInfo] = useState(null) 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 [passwordModalVisible, setPasswordModalVisible] = useState(false) const [password, setPassword] = useState('') const [accessPassword, setAccessPassword] = useState(null) // 已验证的密码 const [siderCollapsed, setSiderCollapsed] = useState(false) const [mobileDrawerVisible, setMobileDrawerVisible] = useState(false) const [isMobile, setIsMobile] = useState(false) 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(() => { const checkMobile = () => { setIsMobile(window.innerWidth < 768) } checkMobile() window.addEventListener('resize', checkMobile) return () => window.removeEventListener('resize', checkMobile) }, []) useEffect(() => { loadProjectInfo() }, [projectId]) // 加载项目基本信息 const loadProjectInfo = async () => { try { const res = await getPreviewInfo(projectId) const info = res.data setProjectInfo(info) if (info.has_password) { // 需要密码验证 setPasswordModalVisible(true) } else { // 无需密码,直接加载文档树 loadFileTree() } } catch (error) { console.error('Load project info error:', error) message.error('项目不存在或已被删除') } } // 验证密码 const handleVerifyPassword = async () => { if (!password.trim()) { message.warning('请输入访问密码') return } try { await verifyAccessPassword(projectId, password) setAccessPassword(password) setPasswordModalVisible(false) loadFileTree(password) message.success('验证成功') } catch (error) { message.error('访问密码错误') } } // 加载文件树 const loadFileTree = async (pwd = null) => { try { const res = await getPreviewTree(projectId, pwd || accessPassword) const tree = res.data || [] setFileTree(tree) // 默认打开 README.md const readmeNode = findReadme(tree) if (readmeNode) { setSelectedFile(readmeNode.key) loadMarkdown(readmeNode.key, pwd || accessPassword) } } catch (error) { console.error('Load file tree error:', error) if (error.response?.status === 403) { message.error('访问密码错误或已过期') setPasswordModalVisible(true) } } } // 查找根目录的 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')) { return { key: node.key, label: node.title.replace('.md', ''), icon: , } } else if (node.title && node.title.endsWith('.pdf')) { return { key: node.key, label: node.title, icon: , } } return null }).filter(Boolean) } // 加载 markdown 文件 const loadMarkdown = async (filePath, pwd = null) => { setLoading(true) setTocItems([]) try { const res = await getPreviewFile(projectId, filePath, pwd || accessPassword) setMarkdownContent(res.data?.content || '') // 移动端自动关闭侧边栏 if (isMobile) { setMobileDrawerVisible(false) } // 滚动到顶部 if (contentRef.current) { contentRef.current.scrollTo({ top: 0, behavior: 'smooth' }) } } catch (error) { console.error('Load markdown error:', error) if (error.response?.status === 403) { message.error('访问密码错误或已过期') setPasswordModalVisible(true) } else { 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 - 使用预览API,添加必要的参数 let url = getPreviewDocumentUrl(projectId, key) const params = [] // 如果有密码,添加密码参数 if (accessPassword) { params.push(`access_pass=${encodeURIComponent(accessPassword)}`) } // 如果用户已登录(私密项目需要),添加token参数 const token = localStorage.getItem('access_token') if (token) { params.push(`token=${encodeURIComponent(token)}`) } if (params.length > 0) { url += `?${params.join('&')}` } setPdfUrl(url) setPdfFilename(key.split('/').pop()) setViewMode('pdf') } else { // 加载Markdown文件 setViewMode('markdown') loadMarkdown(key) } } const menuItems = convertTreeToMenuItems(fileTree) // 侧边栏内容 const SiderContent = () => ( <>

{projectInfo?.name || '项目预览'}

{projectInfo?.description && (

{projectInfo.description}

)}
) return (
{/* 移动端使用 Drawer,桌面端使用 Sider */} {isMobile ? ( <> setMobileDrawerVisible(false)} open={mobileDrawerVisible} width="80%" > ) : ( )} {/* 右侧内容区 */}
{loading ? (
加载中...
) : viewMode === 'pdf' ? ( ) : (
{markdownContent}
)}
{/* 返回顶部按钮 - 仅在markdown模式显示 */} {viewMode === 'markdown' && ( } type="primary" style={{ right: tocCollapsed || isMobile ? 24 : 280 }} onClick={() => { if (contentRef.current) { contentRef.current.scrollTo({ top: 0, behavior: 'smooth' }) } }} /> )}
{/* 右侧TOC面板(仅桌面端且markdown模式显示) */} {!isMobile && viewMode === 'markdown' && !tocCollapsed && (

文档索引

{tocItems.length > 0 ? ( contentRef.current} items={tocItems.map((item) => ({ key: item.key, href: item.href, title: (
{item.title}
), }))} /> ) : (
当前文档无标题
)}
)}
{/* TOC展开按钮(仅桌面端) */} {!isMobile && tocCollapsed && ( )} {/* 密码验证模态框 */} 访问验证
} open={passwordModalVisible} onOk={handleVerifyPassword} onCancel={() => setPasswordModalVisible(false)} okText="验证" cancelText="取消" maskClosable={false} >

该项目需要访问密码,请输入密码后继续浏览。

setPassword(e.target.value)} onPressEnter={handleVerifyPassword} prefix={} />
) } export default PreviewPage