diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index fced962..c56059f 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -1,254 +1,9 @@ -# NexDocus 功能增强实现计划 +## Stage 1: Add Back Icon to Project Detail View +**Goal**: Provide a clear way for users to return to the project list or previous page from the Project Browse Mode (DocumentPage). +**Success Criteria**: A Close icon is visible in the Sider Header and navigates the user back using history or fallback. +**Status**: Complete -## Stage 1: 路由调整和项目成员管理 -**Goal**: 调整路由结构,实现项目卡片上的成员管理功能 -**Success Criteria**: -- /projects/my 路由正常工作 -- /projects/share 路由正常工作 -- 项目卡片上可以打开成员管理弹窗 -- 可以添加/删除项目成员 -**Tests**: -- 访问 /projects/my 显示我创建的项目 -- 访问 /projects/share 显示我参与的项目 -- 点击项目卡片的成员图标打开成员管理弹窗 -- 成功添加用户到项目 -- 成功从项目删除用户 -**Status**: ✅ Completed - ---- - -## Stage 2: 个人桌面统计增强 -**Goal**: 在个人桌面增加"参加项目数"统计 -**Success Criteria**: -- 个人桌面显示参加项目数 -- 统计数据准确(我创建的项目 + 我参与的项目) -**Tests**: -- 创建新项目后,个人项目数 +1 -- 加入协作项目后,参加项目数 +1 -- 统计卡片正确显示两个数值 -**Status**: ✅ Completed - ---- - -## Stage 3: 参与项目视图和权限控制 -**Goal**: 实现参与项目单独视图,区分所有者和成员权限 -**Success Criteria**: -- /projects/share 页面单独展示我参与的项目(非所有者) -- 项目卡片根据角色显示不同操作选项 -- 成员不能删除项目(删除按钮不显示) -- 成员可以编辑项目内文档 -**Tests**: -- 参与者打开 /projects/share 只看到协作项目 -- 参与者看不到删除项目按钮 -- 参与者可以正常编辑文档 -**Status**: ✅ Completed - ---- - -## Stage 4: 操作日志功能 -**Goal**: 为文档编辑操作记录日志 -**Success Criteria**: -- 创建者编辑文档时记录到 operation_logs 表 -- 参与者编辑文档时记录到 operation_logs 表 -- 日志包含用户ID、操作类型、资源信息 -**Tests**: -- 编辑文档后,operation_logs 表有新记录 -- 日志包含正确的用户ID和文档路径 -- 不同用户编辑生成不同日志 -**Status**: ✅ Completed - ---- - -## Stage 5: 项目编辑功能 -**Goal**: 在我的项目卡片上增加编辑功能 -**Success Criteria**: -- 项目卡片显示编辑按钮(仅所有者) -- 点击编辑弹出表单 -- 可以修改项目名称、描述、公开性 -**Tests**: -- 项目所有者看到编辑按钮 -- 编辑项目信息成功保存 -- 非所有者不显示编辑按钮 -**Status**: ✅ Completed - ---- - -## Stage 6: 全局搜索功能 -**Goal**: 实现全局搜索文档内容 -**Success Criteria**: -- 后端提供搜索API(搜索文档元数据+内容) -- 前端增加全局搜索框 -- 搜索结果显示匹配的文档列表 -- 点击结果跳转到对应文档 -**Tests**: -- 搜索文档标题能找到对应文档 -- 搜索文档内容能找到包含关键词的文档 -- 搜索结果高亮显示关键词 -- 点击搜索结果正确跳转 -**Status**: ✅ Completed - ---- - -## Stage 7: PDF文档混合模式 -**Goal**: 支持项目中上传PDF文件,并在Markdown和浏览模式下查看PDF - -### 7.1 后端PDF文件上传支持 -**Goal**: 后端支持PDF文件上传并存储到_assets/files目录 - -**实现内容**: -1. 修改`/files/{project_id}/upload-file`接口,支持PDF格式(.pdf) -2. 扩展文件类型验证,允许`application/pdf` -3. PDF文件存储到`_assets/files/`目录 -4. 返回PDF文件的访问路径 - -**Success Criteria**: -- 可以通过API上传PDF文件 -- PDF文件正确存储到_assets/files目录 -- 返回可访问的文件路径 -- 上传其他格式文件时报错 - -**Tests**: -- 使用Postman上传PDF文件,验证返回路径 -- 验证文件确实存储在正确目录 -- 尝试上传非法格式,验证错误提示 - -**Status**: ✅ Completed - ---- - -### 7.2 文件树显示PDF文件 -**Goal**: 前端文件树能够识别和显示PDF文件 - -**实现内容**: -1. 修改`StorageService.generate_tree()`,包含_assets/files目录中的PDF文件 -2. 前端文件树组件识别.pdf文件 -3. PDF文件显示专用图标(FilePdfOutlined) -4. 文件树节点携带文件类型信息 - -**Success Criteria**: -- 文件树中显示PDF文件节点 -- PDF文件有独特的图标 -- 点击PDF文件能触发相应事件 - -**Tests**: -- 在项目中上传PDF后,刷新文件树查看是否显示 -- 验证PDF文件图标是否正确 -- 点击PDF文件查看是否有响应 - -**Status**: ✅ Completed - ---- - -### 7.3 Markdown编辑器支持插入PDF链接 -**Goal**: 在MD编辑器中可以方便地插入PDF文件链接 - -**实现内容**: -1. 编辑器工具栏添加"插入PDF"按钮 -2. 点击按钮弹出上传对话框 -3. 上传成功后自动在光标位置插入Markdown链接:`[文件名.pdf](/_assets/files/xxx.pdf)` -4. 支持拖拽PDF文件到编辑器(可选) - -**Success Criteria**: -- 工具栏有"插入PDF"按钮 -- 可以上传PDF并自动插入链接 -- 插入的链接格式正确 -- 编辑器预览时显示PDF链接 - -**Tests**: -- 点击"插入PDF"按钮,上传文件 -- 验证插入的Markdown链接格式 -- 保存后重新打开,验证链接是否保留 - -**Status**: ✅ Completed - ---- - -### 7.4 PDF文件预览功能 -**Goal**: 浏览模式下点击PDF链接或文件树中的PDF文件时,可以在线预览PDF - -**实现内容**: -1. 安装PDF预览库(react-pdf) -2. 创建PDFViewer组件,支持: - - 分页显示 - - 缩放控制(放大/缩小/适应宽度) - - 页码跳转 - - 下载功能 -3. 在浏览模式下,点击PDF链接时打开预览器 -4. 从文件树点击PDF文件时也打开预览器 -5. 后端提供PDF文件访问接口(权限校验) - -**Success Criteria**: -- 可以在浏览器中预览PDF文件 -- 支持翻页、缩放等基本操作 -- 只有有权限的用户可以查看PDF -- PDF预览界面美观易用 - -**Tests**: -- 在浏览模式下点击PDF链接,验证能否打开 -- 测试翻页、缩放、下载功能 -- 测试权限:无权限用户访问PDF时返回403 -- 测试不同大小的PDF文件加载速度 - -**Status**: ✅ Completed - ---- - -### 7.5 搜索集成PDF文件名 -**Goal**: 全局搜索时可以搜索到PDF文件名 - -**实现内容**: -1. 修改`/search/documents`接口,添加对PDF文件的搜索 -2. 搜索结果中显示PDF文件,带"PDF文档"标签 -3. 点击搜索结果中的PDF文件时打开预览 - -**Success Criteria**: -- 搜索PDF文件名能返回结果 -- 搜索结果中PDF文件有明确标识 -- 点击搜索结果可以打开PDF预览 - -**Tests**: -- 上传名为"产品说明书.pdf"的文件 -- 搜索"说明书",验证是否出现在结果中 -- 点击搜索结果,验证是否打开PDF预览 - -**Status**: ✅ Completed - ---- - -## Stage 8: 全文内容检索增强 (Whoosh) -**Goal**: 引入 Whoosh 搜索引擎,实现基于文档内容的全文检索和高亮显示。 - -### 核心方案 -- **技术**: Whoosh (纯Python搜索引擎) + Jieba (中文分词) -- **存储**: 本地文件系统 `storage/search_index` -- **同步**: 实时文件操作钩子 + 批量重建脚本 - -### 8.1 基础设施搭建 -**Goal**: 引入依赖并创建搜索服务 -**Status**: Not Started -**Implementation**: -1. 添加 `Whoosh`, `jieba` 到 requirements.txt -2. 创建 `backend/app/services/search_service.py` - - 定义 Index Schema (project_id, path, title, content) - - 实现 Add/Remove/Update Document 方法 - - 实现 Search 方法(支持高亮) - -### 8.2 索引同步机制 -**Goal**: 确保文件修改实时反映到搜索结果 -**Status**: Not Started -**Implementation**: -1. 修改 `backend/app/api/v1/files.py` - - 在 `save_file` 后调用 `index.add_document` - - 在 `operate_file` (rename/move) 后更新索引 - - 在 `operate_file` (delete) 后删除索引 - - 在 `import_documents` 后批量添加索引 -2. 创建初始化脚本/API:`POST /api/v1/system/rebuild-index` - -### 8.3 搜索接口升级 -**Goal**: 升级搜索API支持内容搜索 -**Status**: Not Started -**Implementation**: -1. 修改 `backend/app/api/v1/search.py` -2. 调用 `SearchService.search` -3. 返回包含 `highlight` 摘要的结果结构 -4. 前端展示优化(显示匹配片段) \ No newline at end of file +## Stage 2: Refine Navigation Logic +**Goal**: Ensure the back button handles entries from notifications or direct links correctly. +**Success Criteria**: Uses browser history if available, otherwise defaults to project list. +**Status**: Complete diff --git a/frontend/src/pages/Document/DocumentEditor.jsx b/frontend/src/pages/Document/DocumentEditor.jsx index fc335f3..e24b80c 100644 --- a/frontend/src/pages/Document/DocumentEditor.jsx +++ b/frontend/src/pages/Document/DocumentEditor.jsx @@ -679,7 +679,8 @@ function DocumentEditor() { if (file) { const url = await handleImageUpload(file) if (url) { - ctx.appendBlock(`![image](${url})`) + ctx.editor.replaceSelection(`![image](${url})`) + ctx.editor.focus() Toast.success('成功', '图片上传成功') } } @@ -691,6 +692,13 @@ function DocumentEditor() { ], } + // 捕获编辑器实例的插件 + const editorRefPlugin = { + editorEffect: (ctx) => { + editorCtxRef.current = ctx + }, + } + return [ gfm(), highlight(), @@ -699,6 +707,7 @@ function DocumentEditor() { gemoji(), uploadImagesPlugin, internalLinkPlugin, + editorRefPlugin, ] }, [projectId]) @@ -718,7 +727,13 @@ function DocumentEditor() { if (url) { // 插入markdown图片语法 const imageMarkdown = `![image](${url})` - setFileContent(prev => prev + '\n' + imageMarkdown) + if (editorCtxRef.current && editorCtxRef.current.editor) { + const editor = editorCtxRef.current.editor + editor.replaceSelection(imageMarkdown) + editor.focus() + } else { + setFileContent(prev => prev + '\n' + imageMarkdown) + } Toast.success('成功', '图片上传成功') } } @@ -740,7 +755,13 @@ function DocumentEditor() { if (url) { const imageMarkdown = `![${file.name}](${url})` - setFileContent(prev => prev + '\n' + imageMarkdown) + if (editorCtxRef.current && editorCtxRef.current.editor) { + const editor = editorCtxRef.current.editor + editor.replaceSelection(imageMarkdown) + editor.focus() + } else { + setFileContent(prev => prev + '\n' + imageMarkdown) + } Toast.success('成功', '图片上传成功') } } diff --git a/frontend/src/pages/Document/DocumentPage.jsx b/frontend/src/pages/Document/DocumentPage.jsx index 6d7ffe1..860046d 100644 --- a/frontend/src/pages/Document/DocumentPage.jsx +++ b/frontend/src/pages/Document/DocumentPage.jsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, useMemo } from 'react' 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 { EditOutlined, VerticalAlignTopOutlined, ShareAltOutlined, MenuFoldOutlined, MenuUnfoldOutlined, FileTextOutlined, FolderOutlined, FilePdfOutlined, CopyOutlined, LockOutlined, CloudDownloadOutlined, CloudUploadOutlined, DownOutlined, SearchOutlined } from '@ant-design/icons' +import { EditOutlined, VerticalAlignTopOutlined, ShareAltOutlined, MenuFoldOutlined, MenuUnfoldOutlined, FileTextOutlined, FolderOutlined, FilePdfOutlined, CopyOutlined, LockOutlined, CloudDownloadOutlined, CloudUploadOutlined, DownOutlined, SearchOutlined, CloseOutlined } from '@ant-design/icons' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import rehypeRaw from 'rehype-raw' @@ -67,6 +67,14 @@ function DocumentPage() { loadFileTree() }, [projectId]) + const handleClose = () => { + if (userRole === 'owner') { + navigate('/projects/my') + } else { + navigate('/projects/share') + } + } + // 监听 URL 参数变化,处理文件导航和搜索 useEffect(() => { // 只有当文件树加载完成后才处理导航,否则无法正确展开目录 @@ -756,7 +764,19 @@ function DocumentPage() { {/* 左侧目录 */}
-

{projectName}

+
+

+ {projectName} +

+ +
{/* 只有 owner/admin/editor 可以编辑和Git操作 */} diff --git a/frontend/src/pages/Preview/PreviewPage.jsx b/frontend/src/pages/Preview/PreviewPage.jsx index f173782..771a452 100644 --- a/frontend/src/pages/Preview/PreviewPage.jsx +++ b/frontend/src/pages/Preview/PreviewPage.jsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, useMemo } from 'react' -import { useParams, useSearchParams } from 'react-router-dom' -import { Layout, Menu, Spin, FloatButton, Button, Modal, Input, message, Drawer, Anchor, Empty } from 'antd' -import { VerticalAlignTopOutlined, MenuOutlined, MenuFoldOutlined, MenuUnfoldOutlined, FileTextOutlined, FolderOutlined, FilePdfOutlined, LockOutlined, SearchOutlined } from '@ant-design/icons' +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' @@ -36,7 +36,17 @@ const HighlightText = ({ text, keyword }) => { function PreviewPage() { const { projectId } = useParams() + const navigate = useNavigate() const [searchParams] = useSearchParams() + + const handleClose = () => { + // 检查是否有历史记录可回退 + if (window.history.length > 1) { + navigate(-1) + } else { + navigate('/projects') + } + } const [projectInfo, setProjectInfo] = useState(null) const [fileTree, setFileTree] = useState([]) const [selectedFile, setSelectedFile] = useState('') @@ -473,7 +483,10 @@ function PreviewPage() { +
navigate('/')} + > logo {projectInfo?.name || '项目预览'}
@@ -528,7 +541,10 @@ function PreviewPage() { collapsedWidth={0} >
-
+
navigate('/')} + > logo

{projectInfo?.name || '项目预览'}