diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index c56059f..ea0dbb9 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -1,9 +1,17 @@ -## 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: Auto-append .md Extension +**Goal**: Ensure new files created in the editor default to .md if no extension is provided. +**Files**: `frontend/src/pages/Document/DocumentEditor.jsx` +**Status**: In Progress -## 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 +## Stage 2: Project Ownership Transfer +**Goal**: Allow project owners to transfer ownership to another user. +**Files**: +- `backend/app/api/v1/projects.py` (Add transfer API) +- `frontend/src/api/project.js` (Add frontend API method) +- `frontend/src/pages/ProjectList/ProjectList.jsx` (Add Transfer UI) +**Status**: Not Started + +## Stage 3: Specific File Share Link +**Goal**: Add a context menu option to share specific files if the project is shared. +**Files**: `frontend/src/pages/Document/DocumentEditor.jsx` +**Status**: Not Started \ No newline at end of file diff --git a/backend/app/api/v1/projects.py b/backend/app/api/v1/projects.py index 95e5a4d..1f628c8 100644 --- a/backend/app/api/v1/projects.py +++ b/backend/app/api/v1/projects.py @@ -21,6 +21,7 @@ from app.schemas.project import ( ProjectMemberResponse, ProjectShareSettings, ProjectShareInfo, + ProjectTransfer, ) from app.schemas.response import success_response from app.services.storage import storage_service @@ -263,6 +264,93 @@ async def update_project( return success_response(data=project_data.dict(), message="项目更新成功") +@router.post("/{project_id}/transfer", response_model=dict) +async def transfer_project( + project_id: int, + transfer_in: ProjectTransfer, + request: Request, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """转移项目所有权""" + # 查询项目 + result = await db.execute(select(Project).where(Project.id == project_id)) + project = result.scalar_one_or_none() + + if not project: + raise HTTPException(status_code=404, detail="项目不存在") + + # 只有项目所有者可以转移 + if project.owner_id != current_user.id: + raise HTTPException(status_code=403, detail="只有项目所有者可以转移项目") + + # 检查新所有者是否存在 + user_result = await db.execute(select(User).where(User.id == transfer_in.new_owner_id)) + new_owner = user_result.scalar_one_or_none() + + if not new_owner: + raise HTTPException(status_code=404, detail="目标用户不存在") + + if new_owner.id == current_user.id: + raise HTTPException(status_code=400, detail="不能转移给自己") + + # 1. 如果新所有者已经是成员,删除成员记录 + member_result = await db.execute( + select(ProjectMember).where( + ProjectMember.project_id == project_id, + ProjectMember.user_id == new_owner.id + ) + ) + existing_member = member_result.scalar_one_or_none() + if existing_member: + await db.delete(existing_member) + + # 2. 将旧所有者添加为管理员成员 + # 检查旧所有者是否已经在member表中(理论上owner不在member表中,但为了健壮性检查一下) + old_member_result = await db.execute( + select(ProjectMember).where( + ProjectMember.project_id == project_id, + ProjectMember.user_id == current_user.id + ) + ) + if not old_member_result.scalar_one_or_none(): + old_owner_member = ProjectMember( + project_id=project_id, + user_id=current_user.id, + role="admin", + invited_by=current_user.id + ) + db.add(old_owner_member) + + # 3. 更新项目所有者 + project.owner_id = new_owner.id + + await db.commit() + + # 记录日志 + await log_service.log_project_operation( + db=db, + operation_type=OperationType.UPDATE_PROJECT, + project_id=project_id, + user=current_user, + detail={"action": "transfer_ownership", "new_owner": new_owner.username}, + request=request, + ) + + # 发送通知 + await notification_service.create_notification( + db=db, + user_id=new_owner.id, + title="项目所有权转让", + content=f"用户 {current_user.nickname or current_user.username} 将项目 [{project.name}] 的所有权转让给了您。", + category="project", + link=f"/projects/{project_id}/docs", + type="info" + ) + + return success_response(message="项目所有权转移成功") + + @router.delete("/{project_id}", response_model=dict) async def delete_project( project_id: int, diff --git a/backend/app/schemas/project.py b/backend/app/schemas/project.py index 21e8d04..f8699db 100644 --- a/backend/app/schemas/project.py +++ b/backend/app/schemas/project.py @@ -90,3 +90,8 @@ class ProjectShareInfo(BaseModel): share_url: str = Field(..., description="分享链接") has_password: bool = Field(..., description="是否设置了访问密码") access_pass: Optional[str] = Field(None, description="访问密码(仅项目所有者可见)") + + +class ProjectTransfer(BaseModel): + """转移项目所有权 Schema""" + new_owner_id: int = Field(..., description="新所有者ID") \ No newline at end of file diff --git a/frontend/src/api/project.js b/frontend/src/api/project.js index b97cad9..6204fb2 100644 --- a/frontend/src/api/project.js +++ b/frontend/src/api/project.js @@ -75,6 +75,17 @@ export function deleteProject(projectId) { }) } +/** + * 转移项目所有权 + */ +export function transferProject(projectId, newOwnerId) { + return request({ + url: `/projects/${projectId}/transfer`, + method: 'post', + data: { new_owner_id: newOwnerId }, + }) +} + /** * 获取项目成员 */ diff --git a/frontend/src/pages/Document/DocumentEditor.jsx b/frontend/src/pages/Document/DocumentEditor.jsx index e24b80c..a17465b 100644 --- a/frontend/src/pages/Document/DocumentEditor.jsx +++ b/frontend/src/pages/Document/DocumentEditor.jsx @@ -343,15 +343,21 @@ function DocumentEditor() { new_path: newPath, } } else { + // 自动补全后缀 (仅针对新建文件) + let finalName = newName + if (operationType === 'create_file' && finalName.indexOf('.') === -1) { + finalName += '.md' + } + // 创建操作 - 使用预先计算的父目录路径 - path = creationParentPath ? `${creationParentPath}/${newName}` : newName + path = creationParentPath ? `${creationParentPath}/${finalName}` : finalName // 检查文件是否已存在 const fileExists = checkFileExists(path) if (fileExists) { Modal.confirm({ title: '文件已存在', - content: `文件 "${newName}" 已存在,是否覆盖?`, + content: `文件 "${finalName}" 已存在,是否覆盖?`, okText: '覆盖', cancelText: '取消', onOk: async () => { diff --git a/frontend/src/pages/Document/DocumentPage.jsx b/frontend/src/pages/Document/DocumentPage.jsx index 860046d..64f6675 100644 --- a/frontend/src/pages/Document/DocumentPage.jsx +++ b/frontend/src/pages/Document/DocumentPage.jsx @@ -612,7 +612,10 @@ function DocumentPage() { // 复制分享链接 const handleCopyLink = async () => { if (!shareInfo) return - const fullUrl = `${window.location.origin}${shareInfo.share_url}` + let fullUrl = `${window.location.origin}${shareInfo.share_url}` + if (selectedFile) { + fullUrl += `?file=${encodeURIComponent(selectedFile)}` + } try { if (navigator.clipboard && window.isSecureContext) { @@ -931,7 +934,7 @@ function DocumentPage() { {/* 分享模态框 */} {/* ... keeping the modal ... */} setShareModalVisible(false)} footer={null} @@ -940,9 +943,14 @@ function DocumentPage() { {shareInfo && (
- + diff --git a/frontend/src/pages/ProjectList/ProjectList.jsx b/frontend/src/pages/ProjectList/ProjectList.jsx index 8d397f0..dd5c716 100644 --- a/frontend/src/pages/ProjectList/ProjectList.jsx +++ b/frontend/src/pages/ProjectList/ProjectList.jsx @@ -1,8 +1,8 @@ import { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { Card, Empty, Modal, Form, Input, Row, Col, Space, Button, Switch, message, Select, Table, Tag } from 'antd' -import { PlusOutlined, FolderOutlined, TeamOutlined, EyeOutlined, ShareAltOutlined, CopyOutlined, DeleteOutlined, EditOutlined, FileOutlined, GithubOutlined, CheckOutlined } from '@ant-design/icons' -import { getMyProjects, getOwnedProjects, getSharedProjects, createProject, deleteProject, updateProject, getProjectMembers, addProjectMember, removeProjectMember, getGitRepos, createGitRepo, updateGitRepo, deleteGitRepo } from '@/api/project' +import { PlusOutlined, FolderOutlined, TeamOutlined, EyeOutlined, ShareAltOutlined, CopyOutlined, DeleteOutlined, EditOutlined, FileOutlined, GithubOutlined, CheckOutlined, SwapOutlined } from '@ant-design/icons' +import { getMyProjects, getOwnedProjects, getSharedProjects, createProject, deleteProject, updateProject, getProjectMembers, addProjectMember, removeProjectMember, getGitRepos, createGitRepo, updateGitRepo, deleteGitRepo, transferProject } from '@/api/project' import { getProjectShareInfo, updateShareSettings } from '@/api/share' import { getUserList } from '@/api/users' import { searchDocuments } from '@/api/search' @@ -33,12 +33,54 @@ function ProjectList({ type = 'my' }) { const [editForm] = Form.useForm() const [gitForm] = Form.useForm() const [memberForm] = Form.useForm() + const [transferModalVisible, setTransferModalVisible] = useState(false) + const [transferForm] = Form.useForm() const navigate = useNavigate() useEffect(() => { fetchProjects() }, [type]) + // ... (fetchProjects code) + + const handleOpenTransfer = async () => { + setLoadingMembers(true) + setTransferModalVisible(true) + try { + // 获取用户列表 (排除自己) + const res = await getUserList({ page: 1, page_size: 100, status: 1 }) + const allUsers = res.data || [] + // 过滤掉当前所有者(也就是自己,虽然API也会校验) + setUsers(allUsers.filter(u => u.id !== currentProject.owner_id)) + } catch (error) { + message.error('加载用户列表失败') + } finally { + setLoadingMembers(false) + } + } + + const handleTransfer = async (values) => { + Modal.confirm({ + title: '确认转移', + content: '确定要将项目所有权转移给该用户吗?转移后您将变为管理员,无法再删除项目或转移所有权。', + okText: '确认转移', + okType: 'danger', + onOk: async () => { + try { + await transferProject(currentProject.id, values.new_owner_id) + message.success('项目所有权已转移') + setTransferModalVisible(false) + setEditModalVisible(false) + transferForm.resetFields() + fetchProjects() + } catch (error) { + console.error('Transfer error:', error) + message.error('转移失败: ' + (error.response?.data?.detail || error.message)) + } + } + }) + } + const fetchProjects = async () => { setLoading(true) try { @@ -651,17 +693,26 @@ function ProjectList({ type = 'my' }) { - + + + +