230 lines
7.8 KiB
React
230 lines
7.8 KiB
React
|
|
import { useState, useEffect, useRef } from 'react'
|
|||
|
|
import { useNavigate, useParams } from 'react-router-dom'
|
|||
|
|
import { Layout, Button, Modal, Input, Spin, Anchor } from 'antd'
|
|||
|
|
import { CloseOutlined, LockOutlined, MenuFoldOutlined, MenuUnfoldOutlined, FileTextOutlined } from '@ant-design/icons'
|
|||
|
|
import ReactMarkdown from 'react-markdown'
|
|||
|
|
import remarkGfm from 'remark-gfm'
|
|||
|
|
import rehypeHighlight from 'rehype-highlight'
|
|||
|
|
import rehypeSlug from 'rehype-slug'
|
|||
|
|
import 'highlight.js/styles/github.css'
|
|||
|
|
import GithubSlugger from 'github-slugger'
|
|||
|
|
import Toast from '@/components/Toast/Toast'
|
|||
|
|
import DocFloatActions from '@/components/DocFloatActions/DocFloatActions'
|
|||
|
|
import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
|||
|
|
import {
|
|||
|
|
getFileSharePublicInfo,
|
|||
|
|
verifyFileSharePassword,
|
|||
|
|
getFileShareContent,
|
|||
|
|
exportFileSharePDF,
|
|||
|
|
} from '@/api/share'
|
|||
|
|
import './PreviewPage.css'
|
|||
|
|
|
|||
|
|
const { Content, Sider } = Layout
|
|||
|
|
|
|||
|
|
function FileSharePage() {
|
|||
|
|
const { shareCode } = useParams()
|
|||
|
|
const navigate = useNavigate()
|
|||
|
|
const contentRef = useRef(null)
|
|||
|
|
const [shareInfo, setShareInfo] = useState(null)
|
|||
|
|
const [contentInfo, setContentInfo] = useState(null)
|
|||
|
|
const [loading, setLoading] = useState(true)
|
|||
|
|
const [isMobile, setIsMobile] = useState(false)
|
|||
|
|
const [tocCollapsed, setTocCollapsed] = useState(false)
|
|||
|
|
const [tocItems, setTocItems] = useState([])
|
|||
|
|
const [passwordModalVisible, setPasswordModalVisible] = useState(false)
|
|||
|
|
const [password, setPassword] = useState('')
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
loadFileShare()
|
|||
|
|
}, [shareCode])
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const checkMobile = () => setIsMobile(window.innerWidth < 768)
|
|||
|
|
checkMobile()
|
|||
|
|
window.addEventListener('resize', checkMobile)
|
|||
|
|
return () => window.removeEventListener('resize', checkMobile)
|
|||
|
|
}, [])
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const markdownContent = contentInfo?.type === 'markdown' ? (contentInfo.content || '') : ''
|
|||
|
|
if (!markdownContent) {
|
|||
|
|
setTocItems([])
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const slugger = new GithubSlugger()
|
|||
|
|
const headings = []
|
|||
|
|
markdownContent.split('\n').forEach((line) => {
|
|||
|
|
const match = line.match(/^(#{1,6})\s+(.+)$/)
|
|||
|
|
if (!match) return
|
|||
|
|
const level = match[1].length
|
|||
|
|
const title = match[2].trim()
|
|||
|
|
const key = slugger.slug(title)
|
|||
|
|
headings.push({ key: `#${key}`, href: `#${key}`, title, level })
|
|||
|
|
})
|
|||
|
|
setTocItems(headings)
|
|||
|
|
}, [contentInfo])
|
|||
|
|
|
|||
|
|
const handleClose = () => {
|
|||
|
|
if (window.history.length > 1) {
|
|||
|
|
navigate(-1)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
navigate('/')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const loadFileShare = async () => {
|
|||
|
|
setLoading(true)
|
|||
|
|
try {
|
|||
|
|
const infoRes = await getFileSharePublicInfo(shareCode)
|
|||
|
|
setShareInfo(infoRes.data)
|
|||
|
|
|
|||
|
|
if (infoRes.data.has_password) {
|
|||
|
|
setContentInfo(null)
|
|||
|
|
setPasswordModalVisible(true)
|
|||
|
|
setLoading(false)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const contentRes = await getFileShareContent(shareCode)
|
|||
|
|
setContentInfo(contentRes.data)
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Load file share error:', error)
|
|||
|
|
Toast.error('加载失败', '分享链接不存在或已失效')
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleVerifyPassword = async () => {
|
|||
|
|
if (!password.trim()) {
|
|||
|
|
Toast.warning('提示', '请输入访问密码')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
await verifyFileSharePassword(shareCode, password)
|
|||
|
|
const contentRes = await getFileShareContent(shareCode, password)
|
|||
|
|
setContentInfo(contentRes.data)
|
|||
|
|
setPasswordModalVisible(false)
|
|||
|
|
Toast.success('验证成功')
|
|||
|
|
} catch (error) {
|
|||
|
|
Toast.error('访问密码错误')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleExportPDF = () => {
|
|||
|
|
if (!contentInfo || contentInfo.type === 'pdf') return
|
|||
|
|
window.open(exportFileSharePDF(shareCode), '_blank')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="preview-page file-share-page">
|
|||
|
|
<div className="file-share-shell">
|
|||
|
|
<div className="file-share-header">
|
|||
|
|
<div className="file-share-meta">
|
|||
|
|
<h1>{shareInfo?.name || '文件分享'}</h1>
|
|||
|
|
{shareInfo?.project_name && <p>{shareInfo.project_name}</p>}
|
|||
|
|
</div>
|
|||
|
|
<Button type="text" icon={<CloseOutlined />} onClick={handleClose} className="preview-close-btn" />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Layout className="file-share-content-layout">
|
|||
|
|
<Content className="file-share-content" ref={contentRef}>
|
|||
|
|
{loading ? (
|
|||
|
|
<div className="preview-loading">
|
|||
|
|
<Spin size="large">
|
|||
|
|
<div style={{ marginTop: 16 }}>加载中...</div>
|
|||
|
|
</Spin>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<div className={`preview-content-wrapper ${contentInfo?.type === 'pdf' ? 'pdf-mode' : ''}`}>
|
|||
|
|
{contentInfo?.type === 'pdf' ? (
|
|||
|
|
<VirtualPDFViewer url={contentInfo.document_url} filename={contentInfo.filename} />
|
|||
|
|
) : (
|
|||
|
|
<div className="markdown-body">
|
|||
|
|
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeSlug, rehypeHighlight]}>
|
|||
|
|
{contentInfo?.content || ''}
|
|||
|
|
</ReactMarkdown>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{contentInfo?.type === 'markdown' && (
|
|||
|
|
<DocFloatActions
|
|||
|
|
scrollRef={contentRef}
|
|||
|
|
right={!isMobile && !tocCollapsed ? 280 : 24}
|
|||
|
|
onExportPDF={handleExportPDF}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</Content>
|
|||
|
|
|
|||
|
|
{!isMobile && contentInfo?.type === 'markdown' && !tocCollapsed && (
|
|||
|
|
<Sider width={250} theme="light" className="preview-toc-sider">
|
|||
|
|
<div className="toc-header">
|
|||
|
|
<h3>文档索引</h3>
|
|||
|
|
<Button type="text" size="small" icon={<MenuFoldOutlined />} onClick={() => setTocCollapsed(true)} />
|
|||
|
|
</div>
|
|||
|
|
<div className="toc-content">
|
|||
|
|
{tocItems.length > 0 ? (
|
|||
|
|
<Anchor
|
|||
|
|
affix={false}
|
|||
|
|
offsetTop={0}
|
|||
|
|
getContainer={() => contentRef.current}
|
|||
|
|
items={tocItems.map((item) => ({
|
|||
|
|
key: item.key,
|
|||
|
|
href: item.href,
|
|||
|
|
title: (
|
|||
|
|
<div style={{ paddingLeft: `${(item.level - 1) * 12}px`, display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|||
|
|
<FileTextOutlined style={{ fontSize: '12px', color: '#8c8c8c' }} />
|
|||
|
|
<span>{item.title}</span>
|
|||
|
|
</div>
|
|||
|
|
),
|
|||
|
|
}))}
|
|||
|
|
/>
|
|||
|
|
) : (
|
|||
|
|
<div className="toc-empty">当前文档无标题</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</Sider>
|
|||
|
|
)}
|
|||
|
|
</Layout>
|
|||
|
|
|
|||
|
|
{!isMobile && contentInfo?.type === 'markdown' && tocCollapsed && (
|
|||
|
|
<Button
|
|||
|
|
type="primary"
|
|||
|
|
icon={<MenuUnfoldOutlined />}
|
|||
|
|
className="toc-toggle-btn"
|
|||
|
|
onClick={() => setTocCollapsed(false)}
|
|||
|
|
>
|
|||
|
|
文档索引
|
|||
|
|
</Button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Modal
|
|||
|
|
title={<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><LockOutlined /><span>访问验证</span></div>}
|
|||
|
|
open={passwordModalVisible}
|
|||
|
|
onOk={handleVerifyPassword}
|
|||
|
|
onCancel={() => setPasswordModalVisible(false)}
|
|||
|
|
okText="验证"
|
|||
|
|
cancelText="取消"
|
|||
|
|
maskClosable={false}
|
|||
|
|
>
|
|||
|
|
<div style={{ marginTop: 16 }}>
|
|||
|
|
<p>该文件分享需要访问密码,请输入密码后继续浏览。</p>
|
|||
|
|
<Input.Password
|
|||
|
|
placeholder="请输入访问密码"
|
|||
|
|
value={password}
|
|||
|
|
onChange={(e) => setPassword(e.target.value)}
|
|||
|
|
onPressEnter={handleVerifyPassword}
|
|||
|
|
prefix={<LockOutlined />}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</Modal>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default FileSharePage
|