0.9.5
parent
9a431fc046
commit
1319310b5a
|
|
@ -5,6 +5,9 @@ server {
|
|||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# 增加上传文件大小限制(支持大文件上传)
|
||||
client_max_body_size 100M;
|
||||
|
||||
# Gzip 压缩
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
|
|
@ -25,6 +28,9 @@ server {
|
|||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
|
||||
# 增加上传文件大小限制
|
||||
client_max_body_size 100M;
|
||||
|
||||
# WebSocket 支持
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
|
|
@ -35,6 +41,15 @@ server {
|
|||
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 / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/* 覆盖MainLayout的content-wrapper padding */
|
||||
.document-editor-page {
|
||||
height: calc(100vh - 64px);
|
||||
width: calc(100% + 32px);
|
||||
height: calc(90vh);
|
||||
/* width: calc(100% + 32px); */
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
|
@ -95,6 +95,20 @@
|
|||
height: 100%;
|
||||
display: block;
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState, useEffect, useRef, useMemo } from 'react'
|
||||
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 {
|
||||
FileOutlined,
|
||||
FolderOutlined,
|
||||
|
|
@ -36,6 +36,7 @@ import {
|
|||
exportDirectory,
|
||||
uploadDocument,
|
||||
} from '@/api/file'
|
||||
import Toast from '@/components/Toast/Toast'
|
||||
import './DocumentEditor.css'
|
||||
|
||||
const { Sider, Content } = Layout
|
||||
|
|
@ -60,6 +61,11 @@ function DocumentEditor() {
|
|||
const [dirOptions, setDirOptions] = useState([])
|
||||
const [editorHeight, setEditorHeight] = useState(600) // 设置初始高度为600px
|
||||
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(() => {
|
||||
|
|
@ -88,7 +94,7 @@ function DocumentEditor() {
|
|||
const tree = data.tree || data || [] // 兼容新旧格式
|
||||
setTreeData(tree)
|
||||
} catch (error) {
|
||||
message.error('加载文件树失败')
|
||||
Toast.error('加载失败', '加载文件树失败')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,7 +107,7 @@ function DocumentEditor() {
|
|||
|
||||
// 检查是否是PDF文件
|
||||
if (filePath.toLowerCase().endsWith('.pdf')) {
|
||||
message.info('PDF文件请在浏览模式下查看')
|
||||
Toast.info('提示', 'PDF文件请在浏览模式下查看')
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +117,7 @@ function DocumentEditor() {
|
|||
setSelectedFile(filePath)
|
||||
setFileContent(res.data.content)
|
||||
} catch (error) {
|
||||
console.error('Load file error:', error)
|
||||
// 错误已通过request interceptor处理
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
|
@ -139,22 +145,28 @@ function DocumentEditor() {
|
|||
|
||||
// 始终记录选中的节点(包括文件夹)
|
||||
setSelectedNode(node)
|
||||
setSelectedMenuKey(key) // 高亮显示选中项
|
||||
|
||||
// 只处理文件(叶子节点)加载内容
|
||||
if (node.isLeaf) {
|
||||
// 检查是否是PDF文件
|
||||
if (key.toLowerCase().endsWith('.pdf')) {
|
||||
message.info('PDF文件请在浏览模式下查看')
|
||||
setSelectedFile(key)
|
||||
setIsPdfSelected(true)
|
||||
setFileContent('')
|
||||
return
|
||||
}
|
||||
|
||||
// 重置PDF标记
|
||||
setIsPdfSelected(false)
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getFileContent(projectId, key)
|
||||
setSelectedFile(key)
|
||||
setFileContent(res.data.content)
|
||||
} catch (error) {
|
||||
message.error('加载文件失败')
|
||||
Toast.error('加载失败', '加载文件失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
|
@ -163,7 +175,7 @@ function DocumentEditor() {
|
|||
|
||||
const handleSaveFile = async () => {
|
||||
if (!selectedFile) {
|
||||
message.warning('请先选择文件')
|
||||
Toast.warning('提示', '请先选择文件')
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -173,9 +185,9 @@ function DocumentEditor() {
|
|||
path: selectedFile,
|
||||
content: fileContent,
|
||||
})
|
||||
message.success('保存成功')
|
||||
Toast.success('成功', '保存成功')
|
||||
} catch (error) {
|
||||
console.error('Save file error:', error)
|
||||
// 错误已通过request interceptor处理
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
|
|
@ -211,7 +223,7 @@ function DocumentEditor() {
|
|||
|
||||
const handleDelete = (path = selectedFile) => {
|
||||
if (!path) {
|
||||
message.warning('请先选择文件')
|
||||
Toast.warning('提示', '请先选择文件')
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -224,7 +236,7 @@ function DocumentEditor() {
|
|||
action: 'delete',
|
||||
path: path,
|
||||
})
|
||||
message.success('删除成功')
|
||||
Toast.success('成功', '删除成功')
|
||||
if (selectedFile === path) {
|
||||
setSelectedFile(null)
|
||||
setFileContent('')
|
||||
|
|
@ -232,7 +244,7 @@ function DocumentEditor() {
|
|||
fetchTree()
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error)
|
||||
message.error('删除失败')
|
||||
Toast.error('错误', '删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -243,7 +255,7 @@ function DocumentEditor() {
|
|||
|
||||
// 保护README.md
|
||||
if (fileName === 'README.md' && path.indexOf('/') === -1) {
|
||||
message.error('根目录的README.md不允许重命名')
|
||||
Toast.error('错误', '根目录的README.md不允许重命名')
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -255,7 +267,7 @@ function DocumentEditor() {
|
|||
|
||||
const handleOperation = async () => {
|
||||
if (!newName.trim()) {
|
||||
message.warning('请输入名称')
|
||||
Toast.warning('提示', '请输入名称')
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -301,7 +313,7 @@ function DocumentEditor() {
|
|||
}
|
||||
|
||||
await operateFile(projectId, params)
|
||||
message.success('操作成功')
|
||||
Toast.success('成功', '操作成功')
|
||||
setModalVisible(false)
|
||||
setNewName('')
|
||||
setRightClickNode(null)
|
||||
|
|
@ -314,7 +326,7 @@ function DocumentEditor() {
|
|||
}
|
||||
} catch (error) {
|
||||
console.error('Operation error:', error)
|
||||
message.error('操作失败')
|
||||
Toast.error('错误', '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -344,7 +356,7 @@ function DocumentEditor() {
|
|||
content: operation === 'create_file' ? '# 新文件\n' : undefined,
|
||||
}
|
||||
await operateFile(projectId, params)
|
||||
message.success('操作成功')
|
||||
Toast.success('成功', '操作成功')
|
||||
setModalVisible(false)
|
||||
setNewName('')
|
||||
setRightClickNode(null)
|
||||
|
|
@ -353,16 +365,26 @@ function DocumentEditor() {
|
|||
|
||||
// 上传文档(支持MD和PDF)
|
||||
const handleImportDocuments = async (info) => {
|
||||
// 防止重复触发
|
||||
if (uploadingRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const { fileList } = info
|
||||
|
||||
// 过滤出有效文件
|
||||
const mdFiles = fileList.filter((f) => f.name.endsWith('.md'))
|
||||
const pdfFiles = fileList.filter((f) => f.name.toLowerCase().endsWith('.pdf'))
|
||||
const allFiles = [...mdFiles, ...pdfFiles]
|
||||
|
||||
if (allFiles.length === 0) {
|
||||
message.warning('请选择.md或.pdf格式的文档')
|
||||
Toast.warning('提示', '请选择.md或.pdf格式的文档')
|
||||
return
|
||||
}
|
||||
|
||||
// 设置上传标记
|
||||
uploadingRef.current = true
|
||||
|
||||
// 确定目标路径(如果选中了目录,上传到该目录)
|
||||
const targetPath = selectedNode && !selectedNode.isLeaf ? selectedNode.key : ''
|
||||
|
||||
|
|
@ -394,6 +416,10 @@ function DocumentEditor() {
|
|||
onOk: async () => {
|
||||
await executeImport(mdFiles, pdfFiles, targetPath)
|
||||
},
|
||||
onCancel: () => {
|
||||
// 取消时重置标记
|
||||
uploadingRef.current = false
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -404,14 +430,19 @@ function DocumentEditor() {
|
|||
|
||||
// 执行导入操作
|
||||
const executeImport = async (mdFiles, pdfFiles, targetPath) => {
|
||||
setUploading(true)
|
||||
setUploadProgress(0)
|
||||
|
||||
try {
|
||||
let successCount = 0
|
||||
const totalFiles = mdFiles.length + pdfFiles.length
|
||||
|
||||
// 上传MD文件
|
||||
if (mdFiles.length > 0) {
|
||||
const files = mdFiles.map((f) => f.originFileObj)
|
||||
await importDocuments(projectId, files, targetPath)
|
||||
successCount += files.length
|
||||
setUploadProgress(Math.round((successCount / totalFiles) * 100))
|
||||
}
|
||||
|
||||
// 上传PDF文件
|
||||
|
|
@ -419,18 +450,22 @@ function DocumentEditor() {
|
|||
for (const pdfFile of pdfFiles) {
|
||||
await uploadDocument(projectId, pdfFile.originFileObj, targetPath)
|
||||
successCount++
|
||||
setUploadProgress(Math.round((successCount / totalFiles) * 100))
|
||||
}
|
||||
}
|
||||
|
||||
message.success(`成功上传 ${successCount} 个文档`)
|
||||
Toast.success('成功', `成功上传 ${successCount} 个文档`)
|
||||
fetchTree()
|
||||
// 清除文件选择
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ''
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Import error:', error)
|
||||
message.error('上传失败')
|
||||
Toast.error('错误', '上传失败')
|
||||
} finally {
|
||||
setUploading(false)
|
||||
setUploadProgress(0)
|
||||
uploadingRef.current = false // 重置上传标记
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -463,10 +498,10 @@ function DocumentEditor() {
|
|||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
message.success('导出成功')
|
||||
Toast.success('成功', '导出成功')
|
||||
} catch (error) {
|
||||
console.error('Export error:', error)
|
||||
message.error('导出失败')
|
||||
Toast.error('错误', '导出失败')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -516,7 +551,7 @@ function DocumentEditor() {
|
|||
path: rightClickNode,
|
||||
new_path: newPath,
|
||||
})
|
||||
message.success('移动成功')
|
||||
Toast.success('成功', '移动成功')
|
||||
setMoveModalVisible(false)
|
||||
setRightClickNode(null)
|
||||
fetchTree()
|
||||
|
|
@ -528,7 +563,7 @@ function DocumentEditor() {
|
|||
}
|
||||
} catch (error) {
|
||||
console.error('Move error:', error)
|
||||
message.error('移动失败')
|
||||
Toast.error('错误', '移动失败')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -540,7 +575,7 @@ function DocumentEditor() {
|
|||
return res.data.url
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error)
|
||||
message.error('图片上传失败')
|
||||
Toast.error('错误', '图片上传失败')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -564,12 +599,10 @@ function DocumentEditor() {
|
|||
input.onchange = async (e) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (file) {
|
||||
message.loading('上传图片中...', 0)
|
||||
const url = await handleImageUpload(file)
|
||||
message.destroy()
|
||||
if (url) {
|
||||
ctx.appendBlock(``)
|
||||
message.success('图片上传成功')
|
||||
Toast.success('成功', '图片上传成功')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -601,15 +634,13 @@ function DocumentEditor() {
|
|||
event.preventDefault()
|
||||
const file = item.getAsFile()
|
||||
if (file) {
|
||||
message.loading('上传图片中...', 0)
|
||||
const url = await handleImageUpload(file)
|
||||
message.destroy()
|
||||
|
||||
if (url) {
|
||||
// 插入markdown图片语法
|
||||
const imageMarkdown = ``
|
||||
setFileContent(prev => prev + '\n' + imageMarkdown)
|
||||
message.success('图片上传成功')
|
||||
Toast.success('成功', '图片上传成功')
|
||||
}
|
||||
}
|
||||
break
|
||||
|
|
@ -626,14 +657,12 @@ function DocumentEditor() {
|
|||
const file = files[i]
|
||||
if (file.type.indexOf('image') !== -1) {
|
||||
event.preventDefault()
|
||||
message.loading('上传图片中...', 0)
|
||||
const url = await handleImageUpload(file)
|
||||
message.destroy()
|
||||
|
||||
if (url) {
|
||||
const imageMarkdown = ``
|
||||
setFileContent(prev => prev + '\n' + imageMarkdown)
|
||||
message.success('图片上传成功')
|
||||
Toast.success('成功', '图片上传成功')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -693,7 +722,7 @@ function DocumentEditor() {
|
|||
const convertTreeToMenuItems = (nodes) => {
|
||||
return nodes.map((node) => {
|
||||
// 使用Dropdown包裹label,实现右键菜单
|
||||
// 使用 div 和 width: 100% 增加点击区域
|
||||
const isSelected = selectedMenuKey === node.key
|
||||
const labelContent = (
|
||||
<Dropdown
|
||||
menu={{ items: getNodeMenuItems(node) }}
|
||||
|
|
@ -706,12 +735,17 @@ function DocumentEditor() {
|
|||
)
|
||||
|
||||
if (!node.isLeaf) {
|
||||
// 目录
|
||||
// 目录 - 通过className和style控制选中样式
|
||||
return {
|
||||
key: node.key,
|
||||
label: labelContent,
|
||||
icon: <FolderOutlined />,
|
||||
icon: <FolderOutlined style={isSelected ? { color: '#1890ff' } : {}} />,
|
||||
children: node.children ? convertTreeToMenuItems(node.children) : [],
|
||||
className: isSelected ? 'folder-selected' : '',
|
||||
onTitleClick: () => {
|
||||
setSelectedNode(node)
|
||||
setSelectedMenuKey(node.key)
|
||||
},
|
||||
}
|
||||
} else if (node.title && node.title.endsWith('.md')) {
|
||||
// Markdown 文件
|
||||
|
|
@ -800,9 +834,16 @@ function DocumentEditor() {
|
|||
</Tooltip>
|
||||
</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
|
||||
mode="inline"
|
||||
selectedKeys={[selectedFile]}
|
||||
selectedKeys={selectedMenuKey ? [selectedMenuKey] : []}
|
||||
openKeys={openKeys}
|
||||
onOpenChange={setOpenKeys}
|
||||
items={menuItems}
|
||||
|
|
@ -836,7 +877,11 @@ function DocumentEditor() {
|
|||
</div>
|
||||
|
||||
<div className="editor-container">
|
||||
{selectedFile ? (
|
||||
{isPdfSelected ? (
|
||||
<div className="empty-editor">
|
||||
<p>PDF文件请在浏览模式下查看</p>
|
||||
</div>
|
||||
) : selectedFile ? (
|
||||
<div
|
||||
className="bytemd-wrapper"
|
||||
onPaste={handlePaste}
|
||||
|
|
@ -867,9 +912,11 @@ function DocumentEditor() {
|
|||
|
||||
<Modal
|
||||
title={
|
||||
operationType === 'create_file' ? '创建文件' :
|
||||
operationType === 'create_dir' ? '创建文件夹' :
|
||||
'重命名'
|
||||
operationType === 'create_file'
|
||||
? `创建文件 (dir: /${creationParentPath || '/'})`
|
||||
: operationType === 'create_dir'
|
||||
? `创建文件夹 (dir: /${creationParentPath || '/'})`
|
||||
: '重命名'
|
||||
}
|
||||
open={modalVisible}
|
||||
onOk={handleOperation}
|
||||
|
|
|
|||
Loading…
Reference in New Issue