修改兼容性
parent
2255982c2c
commit
08f4115889
Binary file not shown.
|
Before Width: | Height: | Size: 393 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 274 KiB |
|
|
@ -89,62 +89,6 @@
|
|||
|
||||
---
|
||||
|
||||
## 技术决策
|
||||
|
||||
### 数据库变更
|
||||
- 无需修改现有表结构(project_members 和 operation_logs 已存在)
|
||||
|
||||
### 后端API新增
|
||||
1. `GET /projects/my` - 获取我创建的项目
|
||||
2. `GET /projects/shared` - 获取我参与的项目
|
||||
3. `POST /projects/{project_id}/members` - 添加项目成员(已存在,需测试)
|
||||
4. `DELETE /projects/{project_id}/members/{user_id}` - 删除项目成员(需新增)
|
||||
5. `POST /search/documents` - 全局文档搜索(需新增)
|
||||
6. 修改 `POST /files/{project_id}/file` - 增加操作日志记录
|
||||
|
||||
### 前端路由调整
|
||||
- `/projects/my` - 我的项目(原 /projects)
|
||||
- `/projects/share` - 参与的项目(新增)
|
||||
- 保持 `/projects` 作为默认,重定向到 `/projects/my`
|
||||
|
||||
### 权限控制策略
|
||||
- 项目所有者:完全控制(增删改查、成员管理、删除项目)
|
||||
- 项目管理员(ADMIN):文档管理、成员管理
|
||||
- 项目编辑者(EDITOR):文档编辑
|
||||
- 项目查看者(VIEWER):仅查看
|
||||
|
||||
### 操作日志设计
|
||||
```json
|
||||
{
|
||||
"user_id": 1,
|
||||
"username": "admin",
|
||||
"operation_type": "update",
|
||||
"resource_type": "document",
|
||||
"resource_id": 123,
|
||||
"detail": {
|
||||
"project_id": 10,
|
||||
"file_path": "/docs/readme.md",
|
||||
"action": "edit",
|
||||
"changes": "content_modified"
|
||||
},
|
||||
"ip_address": "192.168.1.1",
|
||||
"status": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 全局搜索实现方案
|
||||
1. **基础版本(当前阶段)**:
|
||||
- 搜索 document_meta 表的 title 字段
|
||||
- 读取磁盘文件内容进行关键词匹配
|
||||
- 适用于小规模项目
|
||||
|
||||
2. **未来优化(可选)**:
|
||||
- 使用 Elasticsearch 全文检索
|
||||
- 添加文档内容索引到数据库
|
||||
- 实现增量索引更新
|
||||
|
||||
---
|
||||
|
||||
## Stage 7: PDF文档混合模式
|
||||
**Goal**: 支持项目中上传PDF文件,并在Markdown和浏览模式下查看PDF
|
||||
|
||||
|
|
@ -271,99 +215,40 @@
|
|||
|
||||
---
|
||||
|
||||
## PDF功能技术选型
|
||||
## Stage 8: 全文内容检索增强 (Whoosh)
|
||||
**Goal**: 引入 Whoosh 搜索引擎,实现基于文档内容的全文检索和高亮显示。
|
||||
|
||||
### PDF预览库对比
|
||||
### 核心方案
|
||||
- **技术**: Whoosh (纯Python搜索引擎) + Jieba (中文分词)
|
||||
- **存储**: 本地文件系统 `storage/search_index`
|
||||
- **同步**: 实时文件操作钩子 + 批量重建脚本
|
||||
|
||||
**方案1: react-pdf** (推荐)
|
||||
- 优点:
|
||||
- 轻量级,基于pdf.js封装
|
||||
- API简单易用
|
||||
- 支持分页、缩放、搜索
|
||||
- TypeScript支持良好
|
||||
- 缺点:
|
||||
- 需要配置worker
|
||||
- 大文件可能需要优化
|
||||
- 安装:`npm install react-pdf pdfjs-dist`
|
||||
### 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 方法(支持高亮)
|
||||
|
||||
**方案2: @react-pdf-viewer**
|
||||
- 优点:
|
||||
- 功能更强大(书签、注释、表单)
|
||||
- 开箱即用的工具栏
|
||||
- 插件系统
|
||||
- 缺点:
|
||||
- 包体积较大
|
||||
- 可能功能过剩
|
||||
### 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`
|
||||
|
||||
**方案3: iframe直接嵌入**
|
||||
- 优点:
|
||||
- 最简单,无需额外依赖
|
||||
- 浏览器原生支持
|
||||
- 缺点:
|
||||
- 功能有限,样式难以自定义
|
||||
- 移动端体验差
|
||||
|
||||
### 推荐方案
|
||||
**使用 react-pdf**,理由:
|
||||
1. 轻量级,适合当前项目规模
|
||||
2. 功能足够,易于扩展
|
||||
3. 社区活跃,文档完善
|
||||
|
||||
### 文件存储结构
|
||||
|
||||
```
|
||||
projects/
|
||||
{storage_key}/
|
||||
_assets/
|
||||
images/ # 图片文件(现有)
|
||||
files/ # PDF等文档文件(新增)
|
||||
README.md
|
||||
其他目录和文件
|
||||
```
|
||||
|
||||
### API设计
|
||||
|
||||
**1. 上传PDF文件(扩展现有接口)**
|
||||
```
|
||||
POST /api/v1/files/{project_id}/upload-file
|
||||
```
|
||||
请求参数:
|
||||
- `file`: 文件二进制
|
||||
- `subfolder`: "files"(新增,默认"images")
|
||||
|
||||
返回:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"filename": "abc123.pdf",
|
||||
"original_filename": "产品说明书.pdf",
|
||||
"path": "_assets/files/abc123.pdf",
|
||||
"size": 1024000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**2. 访问PDF文件(新接口)**
|
||||
```
|
||||
GET /api/v1/files/{project_id}/asset?path={relative_path}
|
||||
```
|
||||
- 权限校验:检查用户是否有项目访问权限
|
||||
- 返回:PDF文件流(Content-Type: application/pdf)
|
||||
|
||||
### Markdown链接格式
|
||||
|
||||
```markdown
|
||||
# 项目文档
|
||||
|
||||
查看详细说明:[产品说明书](/_assets/files/abc123.pdf)
|
||||
```
|
||||
|
||||
### 待确认问题
|
||||
|
||||
1. **文件大小限制**:PDF文件最大允许多少MB?建议:20MB
|
||||
2. **是否支持PDF注释/标注**:暂不支持,后续可扩展
|
||||
3. **PDF缩略图**:是否需要在文件树显示PDF缩略图?建议:暂不支持
|
||||
4. **PDF搜索文本内容**:是否需要搜索PDF内部文字?建议:暂不支持
|
||||
5. **并发上传限制**:是否限制同时上传的PDF数量?
|
||||
6. **历史版本**:PDF文件是否需要版本控制?建议:暂不支持
|
||||
### 8.3 搜索接口升级
|
||||
**Goal**: 升级搜索API支持内容搜索
|
||||
**Status**: Not Started
|
||||
**Implementation**:
|
||||
1. 修改 `backend/app/api/v1/search.py`
|
||||
2. 调用 `SearchService.search`
|
||||
3. 返回包含 `highlight` 摘要的结果结构
|
||||
4. 前端展示优化(显示匹配片段)
|
||||
|
|
@ -39,17 +39,16 @@
|
|||
|
||||
.sider-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mode-toggle-btn {
|
||||
border-radius: 6px !important;
|
||||
font-weight: 500 !important;
|
||||
height: 32px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
padding: 0 12px !important;
|
||||
justify-content: center !important;
|
||||
padding: 4px 15px !important;
|
||||
border: none !important;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
|
@ -70,9 +69,6 @@
|
|||
}
|
||||
|
||||
.sider-actions .ant-btn:not(.mode-toggle-btn) {
|
||||
background: #f0f0f0;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ function DocumentEditor() {
|
|||
const [isPdfSelected, setIsPdfSelected] = useState(false) // 是否选中了PDF文件
|
||||
const [linkModalVisible, setLinkModalVisible] = useState(false)
|
||||
const [linkTarget, setLinkTarget] = useState(null)
|
||||
const [projectName, setProjectName] = useState('项目文档') // 项目名称
|
||||
const [projectName, setProjectName] = useState('') // 项目名称
|
||||
const editorCtxRef = useRef(null)
|
||||
|
||||
// 插入内链接
|
||||
|
|
@ -124,7 +124,7 @@ function DocumentEditor() {
|
|||
const res = await getProjectTree(projectId)
|
||||
const data = res.data || {}
|
||||
const tree = data.tree || data || [] // 兼容新旧格式
|
||||
const name = data.project_name || '项目文档'
|
||||
const name = data.project_name
|
||||
setTreeData(tree)
|
||||
setProjectName(name)
|
||||
} catch (error) {
|
||||
|
|
@ -864,55 +864,55 @@ function DocumentEditor() {
|
|||
<div className="sider-header">
|
||||
<h2>{projectName}</h2>
|
||||
<div className="sider-actions">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
className="mode-toggle-btn exit-edit"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => navigate(`/projects/${projectId}/docs`)}
|
||||
>
|
||||
退出编辑
|
||||
</Button>
|
||||
<Tooltip title="添加文件">
|
||||
<Space size={8}>
|
||||
<Button
|
||||
type="text"
|
||||
type="primary"
|
||||
size="middle"
|
||||
icon={<FileAddOutlined />}
|
||||
onClick={handleCreateFile}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="添加目录">
|
||||
<Button
|
||||
type="text"
|
||||
size="middle"
|
||||
icon={<FolderAddOutlined />}
|
||||
onClick={handleCreateDir}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Upload
|
||||
multiple
|
||||
accept=".md,.pdf,application/pdf"
|
||||
showUploadList={false}
|
||||
fileList={fileList}
|
||||
beforeUpload={() => false}
|
||||
onChange={handleImportDocuments}
|
||||
>
|
||||
<Tooltip title="上传文档">
|
||||
<Button
|
||||
type="text"
|
||||
size="middle"
|
||||
icon={<UploadOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Upload>
|
||||
<Tooltip title="导出文档">
|
||||
<Button
|
||||
type="text"
|
||||
size="middle"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={handleExportDirectory}
|
||||
/>
|
||||
</Tooltip>
|
||||
className="mode-toggle-btn exit-edit"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => navigate(`/projects/${projectId}/docs`)}
|
||||
>
|
||||
退出
|
||||
</Button>
|
||||
<Space.Compact>
|
||||
<Tooltip title="添加文件">
|
||||
<Button
|
||||
size="middle"
|
||||
icon={<FileAddOutlined />}
|
||||
onClick={handleCreateFile}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="添加目录">
|
||||
<Button
|
||||
size="middle"
|
||||
icon={<FolderAddOutlined />}
|
||||
onClick={handleCreateDir}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Upload
|
||||
multiple
|
||||
accept=".md,.pdf,application/pdf"
|
||||
showUploadList={false}
|
||||
fileList={fileList}
|
||||
beforeUpload={() => false}
|
||||
onChange={handleImportDocuments}
|
||||
>
|
||||
<Tooltip title="上传文档">
|
||||
<Button
|
||||
size="middle"
|
||||
icon={<UploadOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Upload>
|
||||
<Tooltip title="导出文档">
|
||||
<Button
|
||||
size="middle"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={handleExportDirectory}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space.Compact>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
{/* 上传进度条 */}
|
||||
|
|
|
|||
|
|
@ -35,12 +35,10 @@
|
|||
|
||||
.docs-sider-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mode-toggle-btn {
|
||||
border-radius: 6px !important;
|
||||
font-weight: 500 !important;
|
||||
box-shadow: 0 2px 6px rgba(22, 119, 255, 0.2);
|
||||
background: linear-gradient(135deg, #1677ff 0%, #4096ff 100%) !important;
|
||||
|
|
@ -48,7 +46,8 @@
|
|||
height: 32px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
padding: 0 12px !important;
|
||||
justify-content: center !important;
|
||||
padding: 4px 15px !important;
|
||||
}
|
||||
|
||||
.mode-toggle-btn:hover {
|
||||
|
|
@ -58,9 +57,6 @@
|
|||
}
|
||||
|
||||
.docs-sider-actions .ant-btn:not(.mode-toggle-btn) {
|
||||
background: #f0f0f0;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { Layout, Menu, Spin, FloatButton, Button, Tooltip, message, Anchor, Modal, Input, Switch, Space, Dropdown } from 'antd'
|
||||
import { EditOutlined, VerticalAlignTopOutlined, ShareAltOutlined, MenuFoldOutlined, MenuUnfoldOutlined, FileTextOutlined, FolderOutlined, FilePdfOutlined, CopyOutlined, LockOutlined, CloudDownloadOutlined, CloudUploadOutlined, DownOutlined } from '@ant-design/icons'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
|
@ -20,6 +20,7 @@ const { Sider, Content } = Layout
|
|||
function DocumentPage() {
|
||||
const { projectId } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const [searchParams] = useSearchParams()
|
||||
const [fileTree, setFileTree] = useState([])
|
||||
const [selectedFile, setSelectedFile] = useState('')
|
||||
const [markdownContent, setMarkdownContent] = useState('')
|
||||
|
|
@ -37,7 +38,7 @@ function DocumentPage() {
|
|||
const [pdfFilename, setPdfFilename] = useState('')
|
||||
const [viewMode, setViewMode] = useState('markdown') // 'markdown' or 'pdf'
|
||||
const [gitRepos, setGitRepos] = useState([])
|
||||
const [projectName, setProjectName] = useState('项目文档')
|
||||
const [projectName, setProjectName] = useState('')
|
||||
const contentRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -66,17 +67,50 @@ function DocumentPage() {
|
|||
const data = res.data || {}
|
||||
const tree = data.tree || data || [] // 兼容新旧格式
|
||||
const role = data.user_role || 'viewer'
|
||||
const name = data.project_name || '项目文档'
|
||||
const name = data.project_name
|
||||
|
||||
setFileTree(tree)
|
||||
setUserRole(role)
|
||||
setProjectName(name)
|
||||
|
||||
// 默认打开 README.md
|
||||
const readmeNode = findReadme(tree)
|
||||
if (readmeNode) {
|
||||
setSelectedFile(readmeNode.key)
|
||||
loadMarkdown(readmeNode.key)
|
||||
// 检查 URL 是否指定了文件
|
||||
const fileParam = searchParams.get('file')
|
||||
if (fileParam) {
|
||||
setSelectedFile(fileParam)
|
||||
|
||||
// 展开父目录
|
||||
const parts = fileParam.split('/')
|
||||
const allParentPaths = []
|
||||
let currentPath = ''
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const part = parts[i]
|
||||
currentPath = currentPath ? `${currentPath}/${part}` : part
|
||||
allParentPaths.push(currentPath)
|
||||
}
|
||||
if (allParentPaths.length > 0) {
|
||||
setOpenKeys(prev => [...new Set([...prev, ...allParentPaths])])
|
||||
}
|
||||
|
||||
// 处理 PDF 或 Markdown
|
||||
if (fileParam.toLowerCase().endsWith('.pdf')) {
|
||||
let url = getDocumentUrl(projectId, fileParam)
|
||||
const token = localStorage.getItem('access_token')
|
||||
if (token) {
|
||||
url += `?token=${encodeURIComponent(token)}`
|
||||
}
|
||||
setPdfUrl(url)
|
||||
setPdfFilename(fileParam.split('/').pop())
|
||||
setViewMode('pdf')
|
||||
} else {
|
||||
loadMarkdown(fileParam)
|
||||
}
|
||||
} else {
|
||||
// 默认打开 README.md
|
||||
const readmeNode = findReadme(tree)
|
||||
if (readmeNode) {
|
||||
setSelectedFile(readmeNode.key)
|
||||
loadMarkdown(readmeNode.key)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load file tree error:', error)
|
||||
|
|
@ -385,10 +419,9 @@ function DocumentPage() {
|
|||
if (gitRepos.length <= 1) {
|
||||
// 0 或 1 个仓库,显示普通按钮
|
||||
return (
|
||||
<Space.Compact>
|
||||
<>
|
||||
<Tooltip title="Git Pull">
|
||||
<Button
|
||||
type="text"
|
||||
size="middle"
|
||||
icon={<CloudDownloadOutlined />}
|
||||
onClick={() => handleGitPull()}
|
||||
|
|
@ -396,13 +429,12 @@ function DocumentPage() {
|
|||
</Tooltip>
|
||||
<Tooltip title="Git Push">
|
||||
<Button
|
||||
type="text"
|
||||
size="middle"
|
||||
icon={<CloudUploadOutlined />}
|
||||
onClick={() => handleGitPush()}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space.Compact>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -419,12 +451,32 @@ function DocumentPage() {
|
|||
onClick: () => handleGitPush(repo.id),
|
||||
}))
|
||||
|
||||
if (gitRepos.length <= 1) {
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="Git Pull">
|
||||
<Button
|
||||
size="middle"
|
||||
icon={<CloudDownloadOutlined />}
|
||||
onClick={() => handleGitPull()}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Git Push">
|
||||
<Button
|
||||
size="middle"
|
||||
icon={<CloudUploadOutlined />}
|
||||
onClick={() => handleGitPush()}
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Space.Compact>
|
||||
<>
|
||||
<Dropdown menu={{ items: pullItems }}>
|
||||
<Tooltip title="Git Pull">
|
||||
<Button
|
||||
type="text"
|
||||
size="middle"
|
||||
icon={<CloudDownloadOutlined />}
|
||||
/>
|
||||
|
|
@ -433,13 +485,12 @@ function DocumentPage() {
|
|||
<Dropdown menu={{ items: pushItems }}>
|
||||
<Tooltip title="Git Push">
|
||||
<Button
|
||||
type="text"
|
||||
size="middle"
|
||||
icon={<CloudUploadOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Dropdown>
|
||||
</Space.Compact>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -545,29 +596,30 @@ function DocumentPage() {
|
|||
<div className="docs-sider-header">
|
||||
<h2>{projectName}</h2>
|
||||
<div className="docs-sider-actions">
|
||||
{/* 只有 owner/admin/editor 可以编辑和Git操作 */}
|
||||
{userRole !== 'viewer' && (
|
||||
<>
|
||||
<Space size={8}>
|
||||
{/* 只有 owner/admin/editor 可以编辑和Git操作 */}
|
||||
{userRole !== 'viewer' && (
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
size="middle"
|
||||
className="mode-toggle-btn"
|
||||
icon={<EditOutlined />}
|
||||
onClick={handleEdit}
|
||||
>
|
||||
编辑模式
|
||||
编辑
|
||||
</Button>
|
||||
{renderGitActions()}
|
||||
</>
|
||||
)}
|
||||
<Tooltip title="分享">
|
||||
<Button
|
||||
type="text"
|
||||
size="middle"
|
||||
icon={<ShareAltOutlined />}
|
||||
onClick={handleShare}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Space.Compact>
|
||||
{userRole !== 'viewer' && renderGitActions()}
|
||||
<Tooltip title="分享">
|
||||
<Button
|
||||
size="middle"
|
||||
icon={<ShareAltOutlined />}
|
||||
onClick={handleShare}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space.Compact>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue