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, Pagination } from 'antd' 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' import ListActionBar from '@/components/ListActionBar/ListActionBar' import Toast from '@/components/Toast/Toast' import './ProjectList.css' function ProjectList({ type = 'my' }) { const [projects, setProjects] = useState([]) const [loading, setLoading] = useState(false) const [modalVisible, setModalVisible] = useState(false) const [editModalVisible, setEditModalVisible] = useState(false) const [gitModalVisible, setGitModalVisible] = useState(false) const [shareModalVisible, setShareModalVisible] = useState(false) const [membersModalVisible, setMembersModalVisible] = useState(false) const [currentProject, setCurrentProject] = useState(null) const [shareInfo, setShareInfo] = useState(null) const [hasPassword, setHasPassword] = useState(false) const [password, setPassword] = useState('') const [searchKeyword, setSearchKeyword] = useState('') const [searchResults, setSearchResults] = useState([]) const [searching, setSearching] = useState(false) const [hasSearched, setHasSearched] = useState(false) const [members, setMembers] = useState([]) const [users, setUsers] = useState([]) const [loadingMembers, setLoadingMembers] = useState(false) const [form] = Form.useForm() const [editForm] = Form.useForm() const [gitForm] = Form.useForm() const [memberForm] = Form.useForm() const [transferModalVisible, setTransferModalVisible] = useState(false) const [transferForm] = Form.useForm() const navigate = useNavigate() const [currentPage, setCurrentPage] = useState(1) const pageSize = 8 useEffect(() => { fetchProjects() setCurrentPage(1) }, [type]) useEffect(() => { setCurrentPage(1) }, [searchKeyword]) // ... (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 { let res if (type === 'my') { res = await getOwnedProjects() } else if (type === 'share') { res = await getSharedProjects() } else { res = await getMyProjects() } setProjects(res.data || []) } catch (error) { console.error('Fetch projects error:', error) } finally { setLoading(false) } } const handleCreateProject = async (values) => { try { await createProject(values) Toast.success('创建成功', '项目已创建') setModalVisible(false) form.resetFields() fetchProjects() } catch (error) { console.error('Create project error:', error) } } const handleDeleteProject = async (projectId) => { Modal.confirm({ title: '确认删除', content: '确定要删除这个项目吗?如果项目中存在文件,将无法删除。删除后将无法恢复!', okText: '确定删除', okType: 'danger', cancelText: '取消', onOk: async () => { try { await deleteProject(projectId) Toast.success('删除成功', '项目已删除') fetchProjects() } catch (error) { console.error('Delete project error:', error) const errorMsg = error.response?.data?.detail || error.message || '删除失败' Toast.error('删除失败', errorMsg) } }, }) } // 打开编辑项目 const handleEdit = (e, project) => { e.stopPropagation() setCurrentProject(project) editForm.setFieldsValue({ name: project.name, description: project.description, is_public: project.is_public === 1, }) setEditModalVisible(true) } // 更新项目 const handleUpdateProject = async (values) => { try { await updateProject(currentProject.id, { ...values, is_public: values.is_public ? 1 : 0, }) message.success('项目更新成功') setEditModalVisible(false) editForm.resetFields() fetchProjects() } catch (error) { console.error('Update project error:', error) message.error('项目更新失败') } } const [gitRepos, setGitRepos] = useState([]) const [loadingRepos, setLoadingRepos] = useState(false) const [gitRepoModalVisible, setGitRepoModalVisible] = useState(false) const [editingRepo, setEditingRepo] = useState(null) const [repoForm] = Form.useForm() // 打开Git设置(仓库列表) const handleGitSettings = (e, project) => { e.stopPropagation() setCurrentProject(project) setGitModalVisible(true) fetchGitRepos(project.id) } // 加载Git仓库列表 const fetchGitRepos = async (projectId) => { setLoadingRepos(true) try { const res = await getGitRepos(projectId) setGitRepos(res.data || []) } catch (error) { console.error('Fetch git repos error:', error) message.error('加载Git仓库失败') } finally { setLoadingRepos(false) } } // 打开添加仓库弹窗 const handleAddRepo = () => { setEditingRepo(null) repoForm.resetFields() // 如果是第一个仓库,默认设为默认 if (gitRepos.length === 0) { repoForm.setFieldsValue({ is_default: 1 }) } setGitRepoModalVisible(true) } // 打开编辑仓库弹窗 const handleEditRepo = (repo) => { setEditingRepo(repo) repoForm.setFieldsValue({ ...repo, is_default: repo.is_default === 1, }) setGitRepoModalVisible(true) } // 删除仓库 const handleDeleteRepo = (repoId) => { Modal.confirm({ title: '确认删除', content: '确定要删除这个Git仓库配置吗?', onOk: async () => { try { await deleteGitRepo(currentProject.id, repoId) message.success('删除成功') fetchGitRepos(currentProject.id) } catch (error) { console.error('Delete repo error:', error) message.error('删除失败') } }, }) } // 保存仓库(新增/更新) const handleSaveRepo = async (values) => { try { const data = { ...values, is_default: values.is_default ? 1 : 0, } if (editingRepo) { await updateGitRepo(currentProject.id, editingRepo.id, data) message.success('更新成功') } else { await createGitRepo(currentProject.id, data) message.success('添加成功') } setGitRepoModalVisible(false) fetchGitRepos(currentProject.id) } catch (error) { console.error('Save repo error:', error) message.error(editingRepo ? '更新失败' : '添加失败') } } const handleOpenProject = (projectId) => { navigate(`/projects/${projectId}/docs`) } // 打开分享设置 const handleShare = async (e, project) => { e.stopPropagation() setCurrentProject(project) try { const res = await getProjectShareInfo(project.id) setShareInfo(res.data) setHasPassword(res.data.has_password) setPassword('') setShareModalVisible(true) } catch (error) { console.error('Get share info error:', error) message.error('获取分享信息失败') } } // 复制分享链接 const handleCopyLink = async () => { if (!shareInfo) return const fullUrl = `${window.location.origin}${shareInfo.share_url}` try { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(fullUrl) Toast.success('复制成功', '分享链接已复制到剪贴板') } else { // Fallback for non-secure contexts or older browsers const textArea = document.createElement("textarea") textArea.value = fullUrl textArea.style.position = "fixed" textArea.style.left = "-9999px" textArea.style.top = "0" document.body.appendChild(textArea) textArea.focus() textArea.select() const successful = document.execCommand('copy') document.body.removeChild(textArea) if (successful) { Toast.success('复制成功', '分享链接已复制到剪贴板') } else { Toast.error('复制失败', '请手动复制链接') } } } catch (err) { console.error('Failed to copy:', err) Toast.error('复制失败', '无法访问剪贴板') } } // 切换密码保护 const handlePasswordToggle = async (checked) => { if (!checked) { // 取消密码 try { await updateShareSettings(currentProject.id, { access_pass: null }) setHasPassword(false) setPassword('') message.success('已取消访问密码') // 刷新分享信息 const res = await getProjectShareInfo(currentProject.id) setShareInfo(res.data) } catch (error) { console.error('Update settings error:', error) message.error('操作失败') } } else { setHasPassword(true) } } // 保存密码 const handleSavePassword = async () => { if (!password.trim()) { message.warning('请输入访问密码') return } try { await updateShareSettings(currentProject.id, { access_pass: password }) message.success('访问密码已设置') // 刷新分享信息 const res = await getProjectShareInfo(currentProject.id) setShareInfo(res.data) setHasPassword(true) } catch (error) { console.error('Save password error:', error) message.error('设置密码失败') } } // 打开成员管理 const handleMembers = async (e, project) => { e.stopPropagation() setCurrentProject(project) setMembersModalVisible(true) setLoadingMembers(true) try { // 并行加载成员列表和用户列表(只获取普通用户 role_id=3) const [membersRes, usersRes] = await Promise.all([ getProjectMembers(project.id), getUserList({ page: 1, page_size: 100, status: 1, role_id: 3 }) ]) console.log('Members Response:', membersRes) console.log('Users Response:', usersRes) const membersData = membersRes.data || [] // 后端返回格式: { code: 200, message: "success", data: [...], total, page, page_size } const usersData = Array.isArray(usersRes.data) ? usersRes.data : [] console.log('Setting members:', membersData) console.log('Setting users:', usersData) setMembers(membersData) setUsers(usersData) } catch (error) { console.error('Get members error:', error) console.error('Error details:', error.response) message.error('获取数据失败: ' + (error.response?.data?.detail || error.message)) } finally { setLoadingMembers(false) } } // 添加成员 const handleAddMember = async (values) => { try { await addProjectMember(currentProject.id, values) message.success('成员添加成功') memberForm.resetFields() // 刷新成员列表(带用户名信息) const res = await getProjectMembers(currentProject.id) setMembers(res.data || []) } catch (error) { console.error('Add member error:', error) const errorMsg = error.response?.data?.detail || error.message || '添加成员失败' message.error(errorMsg) } } // 删除成员 const handleRemoveMember = async (userId) => { Modal.confirm({ title: '确认删除', content: '确定要删除这个成员吗?', onOk: async () => { try { await removeProjectMember(currentProject.id, userId) message.success('成员删除成功') // 刷新成员列表(带用户名信息) const res = await getProjectMembers(currentProject.id) setMembers(res.data || []) } catch (error) { console.error('Remove member error:', error) const errorMsg = error.response?.data?.detail || error.message || '删除成员失败' message.error(errorMsg) } }, }) } // 处理搜索输入变化 const handleSearchChange = (value) => { setSearchKeyword(value) // 如果清空了输入框,重置搜索状态 if (!value || !value.trim()) { setSearchResults([]) setHasSearched(false) } } // 处理搜索 const handleSearch = async (keyword) => { setSearchKeyword(keyword) if (!keyword || !keyword.trim()) { // 清空搜索,显示所有项目 setSearchResults([]) setSearching(false) setHasSearched(false) return } setSearching(true) setHasSearched(true) try { const res = await searchDocuments(keyword.trim()) setSearchResults(res.data || []) } catch (error) { console.error('Search error:', error) Toast.error('搜索失败', error.response?.data?.detail || error.message) } finally { setSearching(false) } } // 处理搜索结果点击 const handleSearchResultClick = (item) => { if (item.type === 'project') { // 跳转到项目文档页 navigate(`/projects/${item.project_id}/docs`) } else if (item.type === 'file') { // 跳转到文件 navigate(`/projects/${item.project_id}/docs?file=${encodeURIComponent(item.file_path)}&keyword=${encodeURIComponent(searchKeyword)}`) } } // 过滤项目(仅在未使用全局搜索时进行本地过滤) const filteredProjects = !hasSearched && searchKeyword ? projects.filter((project) => project.name.toLowerCase().includes(searchKeyword.toLowerCase()) || (project.description && project.description.toLowerCase().includes(searchKeyword.toLowerCase())) ) : projects const paginatedProjects = filteredProjects.slice((currentPage - 1) * pageSize, currentPage * pageSize) return (
{item.project_description || '暂无描述'}
)} {item.type === 'file' && ({project.description || '暂无描述'}