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' && (
}
onClick={handleEdit}
/>
)}
}
onClick={handleShare}
/>
}
onClick={() => message.info('设置功能开发中')}
/>
{/* 右侧内容区 */}
{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 && (
文档索引
}
onClick={() => setTocCollapsed(true)}
/>
{tocItems.length > 0 ? (
contentRef.current}
items={tocItems.map((item) => ({
key: item.key,
href: item.href,
title: (
{item.title}
),
}))}
/>
) : (
当前文档无标题
)}
)}
{/* TOC展开按钮 */}
{tocCollapsed && (
}
className="toc-toggle-btn"
onClick={() => setTocCollapsed(false)}
>
文档索引
)}
{/* 分享模态框 */}
setShareModalVisible(false)}
footer={null}
width={500}
>
{shareInfo && (
}
/>
访问密码保护
{hasPassword && (
setPassword(e.target.value)}
/>
)}
)}
)
}
export default DocumentPage