预览页面增加了pdf导出
parent
e6794a1952
commit
cbef580776
|
|
@ -1,17 +0,0 @@
|
|||
## Stage 1: Auto-append .md Extension
|
||||
**Goal**: Ensure new files created in the editor default to .md if no extension is provided.
|
||||
**Files**: `frontend/src/pages/Document/DocumentEditor.jsx`
|
||||
**Status**: In Progress
|
||||
|
||||
## Stage 2: Project Ownership Transfer
|
||||
**Goal**: Allow project owners to transfer ownership to another user.
|
||||
**Files**:
|
||||
- `backend/app/api/v1/projects.py` (Add transfer API)
|
||||
- `frontend/src/api/project.js` (Add frontend API method)
|
||||
- `frontend/src/pages/ProjectList/ProjectList.jsx` (Add Transfer UI)
|
||||
**Status**: Not Started
|
||||
|
||||
## Stage 3: Specific File Share Link
|
||||
**Goal**: Add a context menu option to share specific files if the project is shared.
|
||||
**Files**: `frontend/src/pages/Document/DocumentEditor.jsx`
|
||||
**Status**: Not Started
|
||||
|
|
@ -14,17 +14,27 @@ ENV PYTHONUNBUFFERED=1 \
|
|||
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources && \
|
||||
sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources
|
||||
|
||||
# 安装系统依赖
|
||||
# 安装系统依赖(含 WeasyPrint 渲染库 + CJK/等宽系统字体)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
default-libmysqlclient-dev \
|
||||
pkg-config \
|
||||
libpango-1.0-0 \
|
||||
libpangoft2-1.0-0 \
|
||||
libpangocairo-1.0-0 \
|
||||
libgdk-pixbuf-2.0-0 \
|
||||
libcairo2 \
|
||||
libffi-dev \
|
||||
shared-mime-info \
|
||||
fontconfig \
|
||||
fonts-dejavu-core \
|
||||
fonts-wqy-microhei \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 复制依赖文件
|
||||
COPY requirements.txt .
|
||||
|
||||
# 使用清华源安装 Python 依赖
|
||||
# 安装 Python 依赖
|
||||
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
# 复制项目文件
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@
|
|||
项目预览相关 API(支持公开和私密项目)
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Header
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from fastapi.security import HTTPAuthorizationCredentials
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import Optional
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.deps import get_current_user_optional, security_optional
|
||||
|
|
@ -17,6 +18,7 @@ from app.models.project import Project, ProjectMember
|
|||
from app.models.user import User
|
||||
from app.schemas.response import success_response
|
||||
from app.services.storage import storage_service
|
||||
from app.services.pdf_service import pdf_service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
|
@ -231,3 +233,85 @@ async def get_preview_document(
|
|||
content_type, _ = mimetypes.guess_type(str(file_path))
|
||||
return FileResponse(path=str(file_path), media_type=content_type, filename=file_path.name)
|
||||
|
||||
|
||||
@router.get("/{project_id}/export-pdf")
|
||||
async def export_preview_pdf(
|
||||
project_id: int,
|
||||
path: str,
|
||||
password: Optional[str] = Header(None, alias="X-Access-Password"),
|
||||
access_pass: Optional[str] = None, # 支持密码查询参数
|
||||
token: Optional[str] = None, # 支持token查询参数
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security_optional),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""导出预览项目的文档为 PDF"""
|
||||
# 获取当前用户(支持header或query参数)
|
||||
current_user = None
|
||||
token_str = None
|
||||
|
||||
if credentials:
|
||||
token_str = credentials.credentials
|
||||
elif token:
|
||||
token_str = token
|
||||
|
||||
if token_str:
|
||||
try:
|
||||
user_id_from_redis = await TokenCache.get_user_id(token_str)
|
||||
if user_id_from_redis:
|
||||
payload = decode_access_token(token_str)
|
||||
if payload:
|
||||
user_id_str = payload.get("sub")
|
||||
if user_id_str:
|
||||
user_id = int(user_id_str)
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
current_user = result.scalar_one_or_none()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 查询项目
|
||||
result = await db.execute(select(Project).where(Project.id == project_id))
|
||||
project = result.scalar_one_or_none()
|
||||
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
|
||||
# 检查访问权限
|
||||
await check_preview_access(project, current_user, db)
|
||||
|
||||
# 验证密码
|
||||
provided_password = password or access_pass
|
||||
if project.access_pass:
|
||||
if not provided_password or project.access_pass != provided_password:
|
||||
raise HTTPException(status_code=403, detail="需要提供正确的访问密码")
|
||||
|
||||
# 获取文件
|
||||
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="文件不存在")
|
||||
|
||||
# 读取 Markdown 内容并转换为 PDF
|
||||
content = await storage_service.read_file(file_path)
|
||||
filename = Path(path).stem + ".pdf"
|
||||
|
||||
# 将 markdown 中的 API 图片 URL 转为文件系统相对路径,以便 WeasyPrint 解析
|
||||
import re
|
||||
content = re.sub(r'/api/v1/files/\d+/assets/', '_assets/', content)
|
||||
|
||||
# 生成 PDF 字节流,传入项目根目录作为 base_url
|
||||
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))
|
||||
|
||||
# 中文文件名需要 RFC 5987 编码
|
||||
from urllib.parse import quote
|
||||
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"
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
"""
|
||||
PDF 生成服务 - 基于 WeasyPrint + 系统字体
|
||||
"""
|
||||
import markdown
|
||||
from weasyprint import HTML, CSS
|
||||
from weasyprint.text.fonts import FontConfiguration
|
||||
import io
|
||||
|
||||
|
||||
class PDFService:
|
||||
def __init__(self):
|
||||
self.font_config = FontConfiguration()
|
||||
|
||||
def get_css(self):
|
||||
return """
|
||||
@page {
|
||||
margin: 2cm;
|
||||
@bottom-right {
|
||||
content: counter(page);
|
||||
font-size: 9pt;
|
||||
}
|
||||
}
|
||||
/* 全局:macOS 用 Hiragino Sans GB / Heiti SC,Docker 用 WenQuanYi Micro Hei */
|
||||
* {
|
||||
font-family: "Hiragino Sans GB", "Heiti SC", "WenQuanYi Micro Hei",
|
||||
"Arial Unicode MS", sans-serif !important;
|
||||
}
|
||||
body {
|
||||
font-size: 11pt;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #fff;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 0.3em;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
/* 代码块:等宽字体优先,中文回退到对应平台的 CJK 字体 */
|
||||
pre, code, pre *, code * {
|
||||
font-family: "Menlo", "DejaVu Sans Mono", "Courier New",
|
||||
"Hiragino Sans GB", "Heiti SC", "WenQuanYi Micro Hei", monospace !important;
|
||||
background-color: transparent;
|
||||
}
|
||||
pre {
|
||||
background-color: #f6f8fa !important;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
display: block;
|
||||
}
|
||||
code {
|
||||
background-color: rgba(175, 184, 193, 0.2);
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 6px;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #dfe2e5;
|
||||
padding: 6px 13px;
|
||||
}
|
||||
tr:nth-child(2n) {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
blockquote {
|
||||
border-left: 0.25em solid #dfe2e5;
|
||||
color: #6a737d;
|
||||
padding: 0 1em;
|
||||
margin-left: 0;
|
||||
}
|
||||
"""
|
||||
|
||||
async def md_to_pdf(self, md_content: str, title: str = "Document", base_url: str = None) -> io.BytesIO:
|
||||
"""将 Markdown 转换为 PDF 字节流"""
|
||||
html_content = markdown.markdown(
|
||||
md_content,
|
||||
extensions=['extra', 'codehilite', 'toc', 'tables']
|
||||
)
|
||||
|
||||
full_html = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body class="markdown-body">
|
||||
{html_content}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
pdf_buffer = io.BytesIO()
|
||||
css = CSS(string=self.get_css(), font_config=self.font_config)
|
||||
HTML(string=full_html, base_url=base_url).write_pdf(
|
||||
pdf_buffer,
|
||||
stylesheets=[css],
|
||||
font_config=self.font_config
|
||||
)
|
||||
pdf_buffer.seek(0)
|
||||
return pdf_buffer
|
||||
|
||||
pdf_service = PDFService()
|
||||
|
|
@ -44,4 +44,6 @@ uvicorn==0.27.0
|
|||
uvloop==0.22.1
|
||||
watchfiles==1.1.1
|
||||
websockets==15.0.1
|
||||
Whoosh==2.7.4
|
||||
Whoosh==2.7.4
|
||||
markdown==3.5.2
|
||||
weasyprint==61.2
|
||||
|
|
|
|||
|
|
@ -78,3 +78,11 @@ export function getPreviewDocumentUrl(projectId, path) {
|
|||
const encodedPath = path.split('/').map(part => encodeURIComponent(part)).join('/')
|
||||
return `/api/v1/preview/${projectId}/document/${encodedPath}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出 PDF
|
||||
*/
|
||||
export function exportPDF(projectId, path) {
|
||||
const encodedPath = encodeURIComponent(path)
|
||||
return `/api/v1/preview/${projectId}/export-pdf?path=${encodedPath}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,10 +33,10 @@
|
|||
/* Collapse Trigger */
|
||||
.collapse-trigger {
|
||||
position: absolute;
|
||||
right: -12px;
|
||||
top: 32px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
right: -14px;
|
||||
top: 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 50%;
|
||||
|
|
@ -45,14 +45,24 @@
|
|||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
color: var(--text-color-secondary);
|
||||
transition: all 0.3s;
|
||||
font-size: 12px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modern-sidebar:hover .collapse-trigger,
|
||||
.collapse-trigger:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.collapse-trigger:hover {
|
||||
color: #1677ff;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
color: #fff;
|
||||
background: #1677ff;
|
||||
border-color: #1677ff;
|
||||
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.35);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Menu Area */
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
VerticalAlignTopOutlined,
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
CloudDownloadOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import 'react-pdf/dist/Page/AnnotationLayer.css'
|
||||
import 'react-pdf/dist/Page/TextLayer.css'
|
||||
|
|
@ -149,6 +150,15 @@ function VirtualPDFViewer({ url, filename }) {
|
|||
}
|
||||
}
|
||||
|
||||
const handleDownload = () => {
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename || 'document.pdf'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="virtual-pdf-viewer-container">
|
||||
{/* 工具栏 */}
|
||||
|
|
@ -187,6 +197,13 @@ function VirtualPDFViewer({ url, filename }) {
|
|||
>
|
||||
回到顶部
|
||||
</Button>
|
||||
<Button
|
||||
icon={<CloudDownloadOutlined />}
|
||||
onClick={handleDownload}
|
||||
size="small"
|
||||
>
|
||||
下载PDF
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Space>
|
||||
|
|
|
|||
|
|
@ -366,4 +366,6 @@
|
|||
.markdown-body code {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 打印样式优化已移除,转向后端生成方案 */
|
||||
|
|
@ -1,20 +1,17 @@
|
|||
import { useState, useEffect, useRef, useMemo } from 'react'
|
||||
import { useParams, useSearchParams, useNavigate } from 'react-router-dom'
|
||||
import { Layout, Menu, Spin, FloatButton, Button, Modal, Input, message, Drawer, Anchor, Empty, Tooltip } from 'antd'
|
||||
import { VerticalAlignTopOutlined, MenuOutlined, MenuFoldOutlined, MenuUnfoldOutlined, FileTextOutlined, FolderOutlined, FilePdfOutlined, LockOutlined, SearchOutlined, CloseOutlined } from '@ant-design/icons'
|
||||
import { Viewer } from '@bytemd/react'
|
||||
import gfm from '@bytemd/plugin-gfm'
|
||||
import highlight from '@bytemd/plugin-highlight'
|
||||
import breaks from '@bytemd/plugin-breaks'
|
||||
import frontmatter from '@bytemd/plugin-frontmatter'
|
||||
import gemoji from '@bytemd/plugin-gemoji'
|
||||
import 'bytemd/dist/index.css'
|
||||
import { Layout, Menu, Spin, FloatButton, 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 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 Mark from 'mark.js'
|
||||
import Highlighter from 'react-highlight-words'
|
||||
import GithubSlugger from 'github-slugger'
|
||||
import { getPreviewInfo, getPreviewTree, getPreviewFile, verifyAccessPassword, getPreviewDocumentUrl } from '@/api/share'
|
||||
import { getPreviewInfo, getPreviewTree, getPreviewFile, verifyAccessPassword, getPreviewDocumentUrl, exportPDF } from '@/api/share'
|
||||
import { searchDocuments } from '@/api/search'
|
||||
import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
||||
import './PreviewPage.css'
|
||||
|
|
@ -74,18 +71,6 @@ function PreviewPage() {
|
|||
const contentRef = useRef(null)
|
||||
const viewerRef = useRef(null)
|
||||
|
||||
// ByteMD 插件配置
|
||||
const plugins = useMemo(() => [
|
||||
gfm(),
|
||||
highlight(),
|
||||
breaks(),
|
||||
frontmatter(),
|
||||
gemoji(),
|
||||
{
|
||||
rehype: (p) => p.use(rehypeSlug)
|
||||
}
|
||||
], [])
|
||||
|
||||
// mark.js 高亮
|
||||
useEffect(() => {
|
||||
if (viewerRef.current && viewMode === 'markdown') {
|
||||
|
|
@ -183,14 +168,14 @@ function PreviewPage() {
|
|||
}
|
||||
} catch (error) {
|
||||
console.error('Load project info error:', error)
|
||||
message.error('项目不存在或已被删除')
|
||||
Toast.error('加载失败', '项目不存在或已被删除')
|
||||
}
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const handleVerifyPassword = async () => {
|
||||
if (!password.trim()) {
|
||||
message.warning('请输入访问密码')
|
||||
Toast.warning('提示', '请输入访问密码')
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -199,9 +184,9 @@ function PreviewPage() {
|
|||
setAccessPassword(password)
|
||||
setPasswordModalVisible(false)
|
||||
loadFileTree(password)
|
||||
message.success('验证成功')
|
||||
Toast.success('验证成功')
|
||||
} catch (error) {
|
||||
message.error('访问密码错误')
|
||||
Toast.error('访问密码错误')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -214,7 +199,7 @@ function PreviewPage() {
|
|||
} catch (error) {
|
||||
console.error('Load file tree error:', error)
|
||||
if (error.response?.status === 403) {
|
||||
message.error('访问密码错误或已过期')
|
||||
Toast.error('访问密码错误或已过期')
|
||||
setPasswordModalVisible(true)
|
||||
}
|
||||
}
|
||||
|
|
@ -247,6 +232,7 @@ function PreviewPage() {
|
|||
setOpenKeys(Array.from(keysToExpand))
|
||||
} catch (error) {
|
||||
console.error('Search error:', error)
|
||||
Toast.error('搜索失败', '请稍后重试')
|
||||
} finally {
|
||||
setIsSearching(false)
|
||||
}
|
||||
|
|
@ -332,10 +318,11 @@ function PreviewPage() {
|
|||
} catch (error) {
|
||||
console.error('Load markdown error:', error)
|
||||
if (error.response?.status === 403) {
|
||||
message.error('访问密码错误或已过期')
|
||||
Toast.error('访问密码错误或已过期')
|
||||
setPasswordModalVisible(true)
|
||||
} else {
|
||||
setMarkdownContent('# 文档加载失败\n\n无法加载该文档,请稍后重试。')
|
||||
Toast.error('加载失败', '文档加载失败,请稍后重试')
|
||||
setMarkdownContent('')
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
|
|
@ -466,6 +453,38 @@ function PreviewPage() {
|
|||
}
|
||||
}
|
||||
|
||||
// 导出 PDF 处理
|
||||
const handleExportPDF = () => {
|
||||
if (viewMode === 'pdf') {
|
||||
// 如果已经是 PDF 文件,直接下载
|
||||
const link = document.createElement('a')
|
||||
link.href = pdfUrl
|
||||
link.download = pdfFilename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
} else {
|
||||
// Markdown 文件:使用后端生成 PDF
|
||||
let url = exportPDF(projectId, selectedFile)
|
||||
const params = []
|
||||
|
||||
if (accessPassword) {
|
||||
params.push(`access_pass=${encodeURIComponent(accessPassword)}`)
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('access_token')
|
||||
if (token) {
|
||||
params.push(`token=${encodeURIComponent(token)}`)
|
||||
}
|
||||
|
||||
if (params.length > 0) {
|
||||
url += (url.includes('?') ? '&' : '?') + params.join('&')
|
||||
}
|
||||
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
const menuItems = convertTreeToMenuItems(filteredTreeData)
|
||||
|
||||
return (
|
||||
|
|
@ -600,25 +619,40 @@ function PreviewPage() {
|
|||
/>
|
||||
) : (
|
||||
<div className="markdown-body" onClick={handleContentClick} ref={viewerRef}>
|
||||
<Viewer
|
||||
value={markdownContent}
|
||||
plugins={plugins}
|
||||
/>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeSlug, rehypeHighlight]}
|
||||
>
|
||||
{markdownContent}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{viewMode === 'markdown' && (
|
||||
<FloatButton
|
||||
icon={<VerticalAlignTopOutlined />}
|
||||
type="primary"
|
||||
style={{ right: tocCollapsed || isMobile ? 24 : 280 }}
|
||||
onClick={() => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
</Content>
|
||||
|
||||
|
|
@ -699,4 +733,4 @@ function PreviewPage() {
|
|||
)
|
||||
}
|
||||
|
||||
export default PreviewPage
|
||||
export default PreviewPage
|
||||
|
|
|
|||
|
|
@ -1,18 +1,10 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import legacy from '@vitejs/plugin-legacy'
|
||||
import path from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
legacy({
|
||||
targets: ['chrome >= 64', 'safari >= 11', 'ios >= 11', 'android >= 9'],
|
||||
additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
|
||||
modernPolyfills: true,
|
||||
}),
|
||||
],
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
|
|
|
|||
Loading…
Reference in New Issue