diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 809bd9e..41ca7d2 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -1,10 +1,13 @@ server { listen 80; server_name localhost; - + 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; diff --git a/frontend/src/pages/Document/DocumentEditor.css b/frontend/src/pages/Document/DocumentEditor.css index 0e6e68b..dd1e6c3 100644 --- a/frontend/src/pages/Document/DocumentEditor.css +++ b/frontend/src/pages/Document/DocumentEditor.css @@ -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, diff --git a/frontend/src/pages/Document/DocumentEditor.jsx b/frontend/src/pages/Document/DocumentEditor.jsx index ec00a62..eb0a587 100644 --- a/frontend/src/pages/Document/DocumentEditor.jsx +++ b/frontend/src/pages/Document/DocumentEditor.jsx @@ -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(`![image](${url})`) - 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 = `![image](${url})` 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 = `![${file.name}](${url})` setFileContent(prev => prev + '\n' + imageMarkdown) - message.success('图片上传成功') + Toast.success('成功', '图片上传成功') } } } @@ -693,10 +722,10 @@ function DocumentEditor() { const convertTreeToMenuItems = (nodes) => { return nodes.map((node) => { // 使用Dropdown包裹label,实现右键菜单 - // 使用 div 和 width: 100% 增加点击区域 + const isSelected = selectedMenuKey === node.key const labelContent = ( -
@@ -706,12 +735,17 @@ function DocumentEditor() { ) if (!node.isLeaf) { - // 目录 + // 目录 - 通过className和style控制选中样式 return { key: node.key, label: labelContent, - icon: , + icon: , 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() {
+ {/* 上传进度条 */} + {uploading && ( +
+
上传中...
+ +
+ )}
- {selectedFile ? ( + {isPdfSelected ? ( +
+

PDF文件请在浏览模式下查看

+
+ ) : selectedFile ? (