修复pdf导出
parent
cbef580776
commit
dc112de840
|
|
@ -684,3 +684,42 @@ async def export_directory(
|
|||
"Content-Disposition": f"attachment; filename={zip_filename}"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{project_id}/export-pdf")
|
||||
async def export_pdf(
|
||||
project_id: int,
|
||||
path: str,
|
||||
request: Request,
|
||||
current_user: User = Depends(get_user_from_token_or_query),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""已登录用户导出 Markdown 为 PDF"""
|
||||
import re
|
||||
from urllib.parse import quote
|
||||
from app.services.pdf_service import pdf_service
|
||||
|
||||
project = await check_project_access(project_id, current_user, db)
|
||||
|
||||
file_path = storage_service.get_secure_path(project.storage_key, path)
|
||||
if not file_path.exists() or not file_path.is_file():
|
||||
raise HTTPException(status_code=404, detail="文件不存在")
|
||||
|
||||
content = await storage_service.read_file(file_path)
|
||||
filename = Path(path).stem + ".pdf"
|
||||
|
||||
# 将 API 图片 URL 转为文件系统相对路径
|
||||
content = re.sub(r'/api/v1/files/\d+/assets/', '_assets/', content)
|
||||
|
||||
project_root = storage_service.get_secure_path(project.storage_key)
|
||||
pdf_buffer = await pdf_service.md_to_pdf(content, title=filename, base_url=str(project_root))
|
||||
|
||||
encoded_filename = quote(filename)
|
||||
return StreamingResponse(
|
||||
pdf_buffer,
|
||||
media_type="application/pdf",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}",
|
||||
"Content-Type": "application/pdf"
|
||||
}
|
||||
)
|
||||
|
|
@ -47,3 +47,4 @@ websockets==15.0.1
|
|||
Whoosh==2.7.4
|
||||
markdown==3.5.2
|
||||
weasyprint==61.2
|
||||
pydyf<0.11.0
|
||||
|
|
|
|||
|
|
@ -129,3 +129,11 @@ export function getDocumentUrl(projectId, path) {
|
|||
const encodedPath = path.split('/').map(part => encodeURIComponent(part)).join('/')
|
||||
return `/api/v1/files/${projectId}/document/${encodedPath}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出 Markdown 为 PDF(已登录用户)
|
||||
*/
|
||||
export function getExportPdfUrl(projectId, path) {
|
||||
const encodedPath = encodeURIComponent(path)
|
||||
return `/api/v1/files/${projectId}/export-pdf?path=${encodedPath}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import { FloatButton, Tooltip } from 'antd'
|
||||
import { MenuOutlined, FilePdfOutlined, VerticalAlignTopOutlined } from '@ant-design/icons'
|
||||
|
||||
/**
|
||||
* 文档浮动操作按钮组(导出 PDF + 回到顶部)
|
||||
* @param {object} props
|
||||
* @param {function} props.onExportPDF - 导出 PDF 回调
|
||||
* @param {React.RefObject} props.scrollRef - 滚动容器 ref
|
||||
* @param {number} [props.right=24] - 距右侧距离
|
||||
*/
|
||||
export default function DocFloatActions({ onExportPDF, scrollRef, right = 24 }) {
|
||||
return (
|
||||
<FloatButton.Group
|
||||
trigger="hover"
|
||||
type="primary"
|
||||
icon={<MenuOutlined />}
|
||||
style={{ right }}
|
||||
>
|
||||
<Tooltip title="导出 PDF" placement="left">
|
||||
<FloatButton
|
||||
icon={<FilePdfOutlined />}
|
||||
onClick={onExportPDF}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="回到顶部" placement="left">
|
||||
<FloatButton
|
||||
icon={<VerticalAlignTopOutlined />}
|
||||
onClick={() => {
|
||||
if (scrollRef?.current) {
|
||||
scrollRef.current.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</FloatButton.Group>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useEffect, useRef, useMemo } from 'react'
|
||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { Layout, Menu, Spin, FloatButton, Button, Tooltip, message, Anchor, Modal, Input, Switch, Space, Dropdown, Empty } from 'antd'
|
||||
import { EditOutlined, VerticalAlignTopOutlined, ShareAltOutlined, MenuFoldOutlined, MenuUnfoldOutlined, FileTextOutlined, FolderOutlined, FilePdfOutlined, CopyOutlined, LockOutlined, CloudDownloadOutlined, CloudUploadOutlined, DownOutlined, SearchOutlined, CloseOutlined } from '@ant-design/icons'
|
||||
import { EditOutlined, VerticalAlignTopOutlined, ShareAltOutlined, MenuFoldOutlined, MenuUnfoldOutlined, FileTextOutlined, FolderOutlined, FilePdfOutlined, CopyOutlined, LockOutlined, CloudDownloadOutlined, CloudUploadOutlined, DownOutlined, SearchOutlined, CloseOutlined, MenuOutlined } from '@ant-design/icons'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
|
|
@ -10,11 +10,12 @@ import rehypeHighlight from 'rehype-highlight'
|
|||
import 'highlight.js/styles/github.css'
|
||||
import Highlighter from 'react-highlight-words'
|
||||
import GithubSlugger from 'github-slugger'
|
||||
import { getProjectTree, getFileContent, getDocumentUrl } from '@/api/file'
|
||||
import { getProjectTree, getFileContent, getDocumentUrl, getExportPdfUrl } from '@/api/file'
|
||||
import { gitPull, gitPush, getGitRepos } from '@/api/project'
|
||||
import { getProjectShareInfo, updateShareSettings } from '@/api/share'
|
||||
import { searchDocuments } from '@/api/search'
|
||||
import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
||||
import DocFloatActions from '@/components/DocFloatActions/DocFloatActions'
|
||||
import Toast from '@/components/Toast/Toast'
|
||||
import './DocumentPage.css'
|
||||
|
||||
|
|
@ -866,16 +867,19 @@ function DocumentPage() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* 返回顶部按钮 - 仅在markdown模式显示 */}
|
||||
{/* 浮动按钮组 - 仅在markdown模式显示 */}
|
||||
{viewMode === 'markdown' && (
|
||||
<FloatButton
|
||||
icon={<VerticalAlignTopOutlined />}
|
||||
type="primary"
|
||||
style={{ right: tocCollapsed ? 24 : 280 }}
|
||||
onClick={() => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
<DocFloatActions
|
||||
scrollRef={contentRef}
|
||||
right={tocCollapsed ? 24 : 280}
|
||||
onExportPDF={() => {
|
||||
if (!selectedFile) return
|
||||
let url = getExportPdfUrl(projectId, selectedFile)
|
||||
const token = localStorage.getItem('access_token')
|
||||
if (token) {
|
||||
url += `&token=${encodeURIComponent(token)}`
|
||||
}
|
||||
window.open(url, '_blank')
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { useState, useEffect, useRef, useMemo } from 'react'
|
||||
import { useParams, useSearchParams, useNavigate } from 'react-router-dom'
|
||||
import { Layout, Menu, Spin, FloatButton, Button, Modal, Input, Drawer, Anchor, Empty, Tooltip } from 'antd'
|
||||
import { Layout, Menu, Spin, Button, Modal, Input, Drawer, Anchor, Empty, Tooltip } from 'antd'
|
||||
import Toast from '@/components/Toast/Toast'
|
||||
import { VerticalAlignTopOutlined, MenuOutlined, MenuFoldOutlined, MenuUnfoldOutlined, FileTextOutlined, FolderOutlined, FilePdfOutlined, LockOutlined, SearchOutlined, CloseOutlined, CloudDownloadOutlined } from '@ant-design/icons'
|
||||
import DocFloatActions from '@/components/DocFloatActions/DocFloatActions'
|
||||
import { MenuFoldOutlined, MenuUnfoldOutlined, FileTextOutlined, FolderOutlined, FilePdfOutlined, LockOutlined, SearchOutlined, CloseOutlined } from '@ant-design/icons'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import rehypeHighlight from 'rehype-highlight'
|
||||
|
|
@ -630,29 +631,11 @@ function PreviewPage() {
|
|||
</div>
|
||||
|
||||
{viewMode === 'markdown' && (
|
||||
<FloatButton.Group
|
||||
trigger="hover"
|
||||
type="primary"
|
||||
icon={<MenuOutlined />}
|
||||
style={{ right: !isMobile && !tocCollapsed ? 280 : 24 }}
|
||||
>
|
||||
<Tooltip title="导出 PDF" placement="left">
|
||||
<FloatButton
|
||||
icon={<FilePdfOutlined />}
|
||||
onClick={handleExportPDF}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="回到顶部" placement="left">
|
||||
<FloatButton
|
||||
icon={<VerticalAlignTopOutlined />}
|
||||
onClick={() => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</FloatButton.Group>
|
||||
<DocFloatActions
|
||||
scrollRef={contentRef}
|
||||
right={!isMobile && !tocCollapsed ? 280 : 24}
|
||||
onExportPDF={handleExportPDF}
|
||||
/>
|
||||
)}
|
||||
</Content>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue