修复pdf导出
parent
cbef580776
commit
dc112de840
|
|
@ -684,3 +684,42 @@ async def export_directory(
|
||||||
"Content-Disposition": f"attachment; filename={zip_filename}"
|
"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
|
Whoosh==2.7.4
|
||||||
markdown==3.5.2
|
markdown==3.5.2
|
||||||
weasyprint==61.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('/')
|
const encodedPath = path.split('/').map(part => encodeURIComponent(part)).join('/')
|
||||||
return `/api/v1/files/${projectId}/document/${encodedPath}`
|
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 { useState, useEffect, useRef, useMemo } from 'react'
|
||||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
|
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 { 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 ReactMarkdown from 'react-markdown'
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
import rehypeRaw from 'rehype-raw'
|
import rehypeRaw from 'rehype-raw'
|
||||||
|
|
@ -10,11 +10,12 @@ import rehypeHighlight from 'rehype-highlight'
|
||||||
import 'highlight.js/styles/github.css'
|
import 'highlight.js/styles/github.css'
|
||||||
import Highlighter from 'react-highlight-words'
|
import Highlighter from 'react-highlight-words'
|
||||||
import GithubSlugger from 'github-slugger'
|
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 { gitPull, gitPush, getGitRepos } from '@/api/project'
|
||||||
import { getProjectShareInfo, updateShareSettings } from '@/api/share'
|
import { getProjectShareInfo, updateShareSettings } from '@/api/share'
|
||||||
import { searchDocuments } from '@/api/search'
|
import { searchDocuments } from '@/api/search'
|
||||||
import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
||||||
|
import DocFloatActions from '@/components/DocFloatActions/DocFloatActions'
|
||||||
import Toast from '@/components/Toast/Toast'
|
import Toast from '@/components/Toast/Toast'
|
||||||
import './DocumentPage.css'
|
import './DocumentPage.css'
|
||||||
|
|
||||||
|
|
@ -866,16 +867,19 @@ function DocumentPage() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 返回顶部按钮 - 仅在markdown模式显示 */}
|
{/* 浮动按钮组 - 仅在markdown模式显示 */}
|
||||||
{viewMode === 'markdown' && (
|
{viewMode === 'markdown' && (
|
||||||
<FloatButton
|
<DocFloatActions
|
||||||
icon={<VerticalAlignTopOutlined />}
|
scrollRef={contentRef}
|
||||||
type="primary"
|
right={tocCollapsed ? 24 : 280}
|
||||||
style={{ right: tocCollapsed ? 24 : 280 }}
|
onExportPDF={() => {
|
||||||
onClick={() => {
|
if (!selectedFile) return
|
||||||
if (contentRef.current) {
|
let url = getExportPdfUrl(projectId, selectedFile)
|
||||||
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' })
|
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 { useState, useEffect, useRef, useMemo } from 'react'
|
||||||
import { useParams, useSearchParams, useNavigate } from 'react-router-dom'
|
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 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 ReactMarkdown from 'react-markdown'
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
import rehypeHighlight from 'rehype-highlight'
|
import rehypeHighlight from 'rehype-highlight'
|
||||||
|
|
@ -630,29 +631,11 @@ function PreviewPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{viewMode === 'markdown' && (
|
{viewMode === 'markdown' && (
|
||||||
<FloatButton.Group
|
<DocFloatActions
|
||||||
trigger="hover"
|
scrollRef={contentRef}
|
||||||
type="primary"
|
right={!isMobile && !tocCollapsed ? 280 : 24}
|
||||||
icon={<MenuOutlined />}
|
onExportPDF={handleExportPDF}
|
||||||
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>
|
|
||||||
)}
|
)}
|
||||||
</Content>
|
</Content>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue