main
mula.liu 2025-12-31 15:12:56 +08:00
parent 9a431fc046
commit 1319310b5a
3 changed files with 124 additions and 48 deletions

View File

@ -1,10 +1,13 @@
server { server {
listen 80; listen 80;
server_name localhost; server_name localhost;
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
# 增加上传文件大小限制(支持大文件上传)
client_max_body_size 100M;
# Gzip 压缩 # Gzip 压缩
gzip on; gzip on;
gzip_vary on; gzip_vary on;
@ -25,6 +28,9 @@ server {
proxy_send_timeout 60s; proxy_send_timeout 60s;
proxy_read_timeout 60s; proxy_read_timeout 60s;
# 增加上传文件大小限制
client_max_body_size 100M;
# WebSocket 支持 # WebSocket 支持
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
@ -35,6 +41,15 @@ server {
add_header Cache-Control "no-cache, no-store, must-revalidate"; add_header Cache-Control "no-cache, no-store, must-revalidate";
} }
# PDF.js worker 文件 - 必须返回正确的 MIME type
location /pdf-worker/ {
types {
application/javascript mjs;
}
add_header Content-Type "application/javascript" always;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# 前端路由支持 # 前端路由支持
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;

View File

@ -1,7 +1,7 @@
/* 覆盖MainLayout的content-wrapper padding */ /* 覆盖MainLayout的content-wrapper padding */
.document-editor-page { .document-editor-page {
height: calc(100vh - 64px); height: calc(90vh);
width: calc(100% + 32px); /* width: calc(100% + 32px); */
display: flex; display: flex;
} }
@ -95,6 +95,20 @@
height: 100%; height: 100%;
display: block; display: block;
user-select: none; user-select: none;
padding: 4px 8px;
margin: -4px -8px;
border-radius: 4px;
transition: all 0.2s;
}
/* 选中的文件夹样式 */
.file-tree .folder-selected > .ant-menu-submenu-title {
background-color: #e6f7ff !important;
color: #1890ff !important;
}
.file-tree .folder-selected > .ant-menu-submenu-title:hover {
background-color: #bae7ff !important;
} }
.file-tree .ant-menu-item, .file-tree .ant-menu-item,

View File

@ -1,6 +1,6 @@
import { useState, useEffect, useRef, useMemo } from 'react' import { useState, useEffect, useRef, useMemo } from 'react'
import { useParams, useNavigate } from 'react-router-dom' import { useParams, useNavigate } from 'react-router-dom'
import { Layout, Menu, Button, message, Modal, Input, Space, Tooltip, Dropdown, Upload, Select } from 'antd' import { Layout, Menu, Button, Modal, Input, Space, Tooltip, Dropdown, Upload, Select, Progress } from 'antd'
import { import {
FileOutlined, FileOutlined,
FolderOutlined, FolderOutlined,
@ -36,6 +36,7 @@ import {
exportDirectory, exportDirectory,
uploadDocument, uploadDocument,
} from '@/api/file' } from '@/api/file'
import Toast from '@/components/Toast/Toast'
import './DocumentEditor.css' import './DocumentEditor.css'
const { Sider, Content } = Layout const { Sider, Content } = Layout
@ -60,6 +61,11 @@ function DocumentEditor() {
const [dirOptions, setDirOptions] = useState([]) const [dirOptions, setDirOptions] = useState([])
const [editorHeight, setEditorHeight] = useState(600) // 600px const [editorHeight, setEditorHeight] = useState(600) // 600px
const [openKeys, setOpenKeys] = useState([]) // Menu const [openKeys, setOpenKeys] = useState([]) // Menu
const [uploadProgress, setUploadProgress] = useState(0) //
const [uploading, setUploading] = useState(false) //
const [selectedMenuKey, setSelectedMenuKey] = useState(null) //
const uploadingRef = useRef(false) // 使ref
const [isPdfSelected, setIsPdfSelected] = useState(false) // PDF
// //
useEffect(() => { useEffect(() => {
@ -88,7 +94,7 @@ function DocumentEditor() {
const tree = data.tree || data || [] // const tree = data.tree || data || [] //
setTreeData(tree) setTreeData(tree)
} catch (error) { } catch (error) {
message.error('加载文件树失败') Toast.error('加载失败', '加载文件树失败')
} }
} }
@ -101,7 +107,7 @@ function DocumentEditor() {
// PDF // PDF
if (filePath.toLowerCase().endsWith('.pdf')) { if (filePath.toLowerCase().endsWith('.pdf')) {
message.info('PDF文件请在浏览模式下查看') Toast.info('提示', 'PDF文件请在浏览模式下查看')
return return
} }
@ -111,7 +117,7 @@ function DocumentEditor() {
setSelectedFile(filePath) setSelectedFile(filePath)
setFileContent(res.data.content) setFileContent(res.data.content)
} catch (error) { } catch (error) {
console.error('Load file error:', error) // request interceptor
} finally { } finally {
setLoading(false) setLoading(false)
} }
@ -139,22 +145,28 @@ function DocumentEditor() {
// //
setSelectedNode(node) setSelectedNode(node)
setSelectedMenuKey(key) //
// //
if (node.isLeaf) { if (node.isLeaf) {
// PDF // PDF
if (key.toLowerCase().endsWith('.pdf')) { if (key.toLowerCase().endsWith('.pdf')) {
message.info('PDF文件请在浏览模式下查看') setSelectedFile(key)
setIsPdfSelected(true)
setFileContent('')
return return
} }
// PDF
setIsPdfSelected(false)
setLoading(true) setLoading(true)
try { try {
const res = await getFileContent(projectId, key) const res = await getFileContent(projectId, key)
setSelectedFile(key) setSelectedFile(key)
setFileContent(res.data.content) setFileContent(res.data.content)
} catch (error) { } catch (error) {
message.error('加载文件失败') Toast.error('加载失败', '加载文件失败')
} finally { } finally {
setLoading(false) setLoading(false)
} }
@ -163,7 +175,7 @@ function DocumentEditor() {
const handleSaveFile = async () => { const handleSaveFile = async () => {
if (!selectedFile) { if (!selectedFile) {
message.warning('请先选择文件') Toast.warning('提示', '请先选择文件')
return return
} }
@ -173,9 +185,9 @@ function DocumentEditor() {
path: selectedFile, path: selectedFile,
content: fileContent, content: fileContent,
}) })
message.success('保存成功') Toast.success('成功', '保存成功')
} catch (error) { } catch (error) {
console.error('Save file error:', error) // request interceptor
} finally { } finally {
setSaving(false) setSaving(false)
} }
@ -211,7 +223,7 @@ function DocumentEditor() {
const handleDelete = (path = selectedFile) => { const handleDelete = (path = selectedFile) => {
if (!path) { if (!path) {
message.warning('请先选择文件') Toast.warning('提示', '请先选择文件')
return return
} }
@ -224,7 +236,7 @@ function DocumentEditor() {
action: 'delete', action: 'delete',
path: path, path: path,
}) })
message.success('删除成功') Toast.success('成功', '删除成功')
if (selectedFile === path) { if (selectedFile === path) {
setSelectedFile(null) setSelectedFile(null)
setFileContent('') setFileContent('')
@ -232,7 +244,7 @@ function DocumentEditor() {
fetchTree() fetchTree()
} catch (error) { } catch (error) {
console.error('Delete error:', error) console.error('Delete error:', error)
message.error('删除失败') Toast.error('错误', '删除失败')
} }
}, },
}) })
@ -243,7 +255,7 @@ function DocumentEditor() {
// README.md // README.md
if (fileName === 'README.md' && path.indexOf('/') === -1) { if (fileName === 'README.md' && path.indexOf('/') === -1) {
message.error('根目录的README.md不允许重命名') Toast.error('错误', '根目录的README.md不允许重命名')
return return
} }
@ -255,7 +267,7 @@ function DocumentEditor() {
const handleOperation = async () => { const handleOperation = async () => {
if (!newName.trim()) { if (!newName.trim()) {
message.warning('请输入名称') Toast.warning('提示', '请输入名称')
return return
} }
@ -301,7 +313,7 @@ function DocumentEditor() {
} }
await operateFile(projectId, params) await operateFile(projectId, params)
message.success('操作成功') Toast.success('成功', '操作成功')
setModalVisible(false) setModalVisible(false)
setNewName('') setNewName('')
setRightClickNode(null) setRightClickNode(null)
@ -314,7 +326,7 @@ function DocumentEditor() {
} }
} catch (error) { } catch (error) {
console.error('Operation error:', error) console.error('Operation error:', error)
message.error('操作失败') Toast.error('错误', '操作失败')
} }
} }
@ -344,7 +356,7 @@ function DocumentEditor() {
content: operation === 'create_file' ? '# 新文件\n' : undefined, content: operation === 'create_file' ? '# 新文件\n' : undefined,
} }
await operateFile(projectId, params) await operateFile(projectId, params)
message.success('操作成功') Toast.success('成功', '操作成功')
setModalVisible(false) setModalVisible(false)
setNewName('') setNewName('')
setRightClickNode(null) setRightClickNode(null)
@ -353,16 +365,26 @@ function DocumentEditor() {
// MDPDF // MDPDF
const handleImportDocuments = async (info) => { const handleImportDocuments = async (info) => {
//
if (uploadingRef.current) {
return
}
const { fileList } = info const { fileList } = info
//
const mdFiles = fileList.filter((f) => f.name.endsWith('.md')) const mdFiles = fileList.filter((f) => f.name.endsWith('.md'))
const pdfFiles = fileList.filter((f) => f.name.toLowerCase().endsWith('.pdf')) const pdfFiles = fileList.filter((f) => f.name.toLowerCase().endsWith('.pdf'))
const allFiles = [...mdFiles, ...pdfFiles] const allFiles = [...mdFiles, ...pdfFiles]
if (allFiles.length === 0) { if (allFiles.length === 0) {
message.warning('请选择.md或.pdf格式的文档') Toast.warning('提示', '请选择.md或.pdf格式的文档')
return return
} }
//
uploadingRef.current = true
// //
const targetPath = selectedNode && !selectedNode.isLeaf ? selectedNode.key : '' const targetPath = selectedNode && !selectedNode.isLeaf ? selectedNode.key : ''
@ -394,6 +416,10 @@ function DocumentEditor() {
onOk: async () => { onOk: async () => {
await executeImport(mdFiles, pdfFiles, targetPath) await executeImport(mdFiles, pdfFiles, targetPath)
}, },
onCancel: () => {
//
uploadingRef.current = false
},
}) })
return return
} }
@ -404,14 +430,19 @@ function DocumentEditor() {
// //
const executeImport = async (mdFiles, pdfFiles, targetPath) => { const executeImport = async (mdFiles, pdfFiles, targetPath) => {
setUploading(true)
setUploadProgress(0)
try { try {
let successCount = 0 let successCount = 0
const totalFiles = mdFiles.length + pdfFiles.length
// MD // MD
if (mdFiles.length > 0) { if (mdFiles.length > 0) {
const files = mdFiles.map((f) => f.originFileObj) const files = mdFiles.map((f) => f.originFileObj)
await importDocuments(projectId, files, targetPath) await importDocuments(projectId, files, targetPath)
successCount += files.length successCount += files.length
setUploadProgress(Math.round((successCount / totalFiles) * 100))
} }
// PDF // PDF
@ -419,18 +450,22 @@ function DocumentEditor() {
for (const pdfFile of pdfFiles) { for (const pdfFile of pdfFiles) {
await uploadDocument(projectId, pdfFile.originFileObj, targetPath) await uploadDocument(projectId, pdfFile.originFileObj, targetPath)
successCount++ successCount++
setUploadProgress(Math.round((successCount / totalFiles) * 100))
} }
} }
message.success(`成功上传 ${successCount} 个文档`) Toast.success('成功', `成功上传 ${successCount} 个文档`)
fetchTree() fetchTree()
// //
if (fileInputRef.current) { if (fileInputRef.current) {
fileInputRef.current.value = '' fileInputRef.current.value = ''
} }
} catch (error) { } catch (error) {
console.error('Import error:', error) Toast.error('错误', '上传失败')
message.error('上传失败') } finally {
setUploading(false)
setUploadProgress(0)
uploadingRef.current = false //
} }
} }
@ -463,10 +498,10 @@ function DocumentEditor() {
document.body.removeChild(link) document.body.removeChild(link)
window.URL.revokeObjectURL(url) window.URL.revokeObjectURL(url)
message.success('导出成功') Toast.success('成功', '导出成功')
} catch (error) { } catch (error) {
console.error('Export error:', error) console.error('Export error:', error)
message.error('导出失败') Toast.error('错误', '导出失败')
} }
} }
@ -516,7 +551,7 @@ function DocumentEditor() {
path: rightClickNode, path: rightClickNode,
new_path: newPath, new_path: newPath,
}) })
message.success('移动成功') Toast.success('成功', '移动成功')
setMoveModalVisible(false) setMoveModalVisible(false)
setRightClickNode(null) setRightClickNode(null)
fetchTree() fetchTree()
@ -528,7 +563,7 @@ function DocumentEditor() {
} }
} catch (error) { } catch (error) {
console.error('Move error:', error) console.error('Move error:', error)
message.error('移动失败') Toast.error('错误', '移动失败')
} }
} }
@ -540,7 +575,7 @@ function DocumentEditor() {
return res.data.url return res.data.url
} catch (error) { } catch (error) {
console.error('Upload error:', error) console.error('Upload error:', error)
message.error('图片上传失败') Toast.error('错误', '图片上传失败')
return null return null
} }
} }
@ -564,12 +599,10 @@ function DocumentEditor() {
input.onchange = async (e) => { input.onchange = async (e) => {
const file = e.target.files?.[0] const file = e.target.files?.[0]
if (file) { if (file) {
message.loading('上传图片中...', 0)
const url = await handleImageUpload(file) const url = await handleImageUpload(file)
message.destroy()
if (url) { if (url) {
ctx.appendBlock(`![image](${url})`) ctx.appendBlock(`![image](${url})`)
message.success('图片上传成功') Toast.success('成功', '图片上传成功')
} }
} }
} }
@ -601,15 +634,13 @@ function DocumentEditor() {
event.preventDefault() event.preventDefault()
const file = item.getAsFile() const file = item.getAsFile()
if (file) { if (file) {
message.loading('上传图片中...', 0)
const url = await handleImageUpload(file) const url = await handleImageUpload(file)
message.destroy()
if (url) { if (url) {
// markdown // markdown
const imageMarkdown = `![image](${url})` const imageMarkdown = `![image](${url})`
setFileContent(prev => prev + '\n' + imageMarkdown) setFileContent(prev => prev + '\n' + imageMarkdown)
message.success('图片上传成功') Toast.success('成功', '图片上传成功')
} }
} }
break break
@ -626,14 +657,12 @@ function DocumentEditor() {
const file = files[i] const file = files[i]
if (file.type.indexOf('image') !== -1) { if (file.type.indexOf('image') !== -1) {
event.preventDefault() event.preventDefault()
message.loading('上传图片中...', 0)
const url = await handleImageUpload(file) const url = await handleImageUpload(file)
message.destroy()
if (url) { if (url) {
const imageMarkdown = `![${file.name}](${url})` const imageMarkdown = `![${file.name}](${url})`
setFileContent(prev => prev + '\n' + imageMarkdown) setFileContent(prev => prev + '\n' + imageMarkdown)
message.success('图片上传成功') Toast.success('成功', '图片上传成功')
} }
} }
} }
@ -693,10 +722,10 @@ function DocumentEditor() {
const convertTreeToMenuItems = (nodes) => { const convertTreeToMenuItems = (nodes) => {
return nodes.map((node) => { return nodes.map((node) => {
// 使Dropdownlabel // 使Dropdownlabel
// 使 div width: 100% const isSelected = selectedMenuKey === node.key
const labelContent = ( const labelContent = (
<Dropdown <Dropdown
menu={{ items: getNodeMenuItems(node) }} menu={{ items: getNodeMenuItems(node) }}
trigger={['contextMenu']} trigger={['contextMenu']}
> >
<div className="tree-node-wrapper"> <div className="tree-node-wrapper">
@ -706,12 +735,17 @@ function DocumentEditor() {
) )
if (!node.isLeaf) { if (!node.isLeaf) {
// // - classNamestyle
return { return {
key: node.key, key: node.key,
label: labelContent, label: labelContent,
icon: <FolderOutlined />, icon: <FolderOutlined style={isSelected ? { color: '#1890ff' } : {}} />,
children: node.children ? convertTreeToMenuItems(node.children) : [], children: node.children ? convertTreeToMenuItems(node.children) : [],
className: isSelected ? 'folder-selected' : '',
onTitleClick: () => {
setSelectedNode(node)
setSelectedMenuKey(node.key)
},
} }
} else if (node.title && node.title.endsWith('.md')) { } else if (node.title && node.title.endsWith('.md')) {
// Markdown // Markdown
@ -800,9 +834,16 @@ function DocumentEditor() {
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
{/* 上传进度条 */}
{uploading && (
<div style={{ padding: '12px 16px', borderBottom: '1px solid #f0f0f0' }}>
<div style={{ marginBottom: 4, fontSize: 12, color: '#666' }}>上传中...</div>
<Progress percent={uploadProgress} size="small" />
</div>
)}
<Menu <Menu
mode="inline" mode="inline"
selectedKeys={[selectedFile]} selectedKeys={selectedMenuKey ? [selectedMenuKey] : []}
openKeys={openKeys} openKeys={openKeys}
onOpenChange={setOpenKeys} onOpenChange={setOpenKeys}
items={menuItems} items={menuItems}
@ -836,7 +877,11 @@ function DocumentEditor() {
</div> </div>
<div className="editor-container"> <div className="editor-container">
{selectedFile ? ( {isPdfSelected ? (
<div className="empty-editor">
<p>PDF文件请在浏览模式下查看</p>
</div>
) : selectedFile ? (
<div <div
className="bytemd-wrapper" className="bytemd-wrapper"
onPaste={handlePaste} onPaste={handlePaste}
@ -867,9 +912,11 @@ function DocumentEditor() {
<Modal <Modal
title={ title={
operationType === 'create_file' ? '创建文件' : operationType === 'create_file'
operationType === 'create_dir' ? '创建文件夹' : ? `创建文件 (dir: /${creationParentPath || '/'})`
'重命名' : operationType === 'create_dir'
? `创建文件夹 (dir: /${creationParentPath || '/'})`
: '重命名'
} }
open={modalVisible} open={modalVisible}
onOk={handleOperation} onOk={handleOperation}