nex_docus/frontend/src/pages/ProjectList/ProjectList.jsx

297 lines
9.1 KiB
React
Raw Normal View History

2025-12-20 11:18:59 +00:00
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { Card, Empty, Modal, Form, Input, Row, Col, Space, Button, Switch, message } from 'antd'
import { PlusOutlined, FolderOutlined, TeamOutlined, EyeOutlined, ShareAltOutlined, CopyOutlined } from '@ant-design/icons'
import { getMyProjects, createProject, deleteProject } from '@/api/project'
import { getProjectShareInfo, updateShareSettings } from '@/api/share'
import MainLayout from '@/components/MainLayout/MainLayout'
import ListActionBar from '@/components/ListActionBar/ListActionBar'
import Toast from '@/components/Toast/Toast'
import './ProjectList.css'
function ProjectList() {
const [projects, setProjects] = useState([])
const [loading, setLoading] = useState(false)
const [modalVisible, setModalVisible] = useState(false)
const [shareModalVisible, setShareModalVisible] = 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 [form] = Form.useForm()
const navigate = useNavigate()
useEffect(() => {
fetchProjects()
}, [])
const fetchProjects = async () => {
setLoading(true)
try {
const 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: '确定要删除这个项目吗?删除后可以在归档中找到',
onOk: async () => {
try {
await deleteProject(projectId)
Toast.success('归档成功', '项目已归档')
fetchProjects()
} catch (error) {
console.error('Delete project error:', error)
}
},
})
}
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 = () => {
if (!shareInfo) return
const fullUrl = `${window.location.origin}${shareInfo.share_url}`
navigator.clipboard.writeText(fullUrl)
message.success('分享链接已复制')
}
// 切换密码保护
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 filteredProjects = projects.filter((project) =>
project.name.toLowerCase().includes(searchKeyword.toLowerCase())
)
return (
<MainLayout>
<div className="project-list-container">
<ListActionBar
actions={[
{
key: 'create',
label: '创建项目',
type: 'primary',
icon: <PlusOutlined />,
onClick: () => setModalVisible(true),
},
]}
search={{
placeholder: '搜索项目...',
value: searchKeyword,
onChange: setSearchKeyword,
onSearch: (value) => setSearchKeyword(value),
}}
showRefresh
onRefresh={fetchProjects}
/>
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
{filteredProjects.map((project) => (
<Col xs={24} sm={12} md={8} lg={6} key={project.id}>
<Card
hoverable
className="project-card"
onClick={() => handleOpenProject(project.id)}
actions={[
<ShareAltOutlined key="share" onClick={(e) => handleShare(e, project)} />,
<EyeOutlined key="view" />,
<TeamOutlined key="members" />,
]}
>
<div className="project-card-icon">
<FolderOutlined style={{ fontSize: 48, color: '#1890ff' }} />
</div>
<h3>{project.name}</h3>
<p className="project-description">{project.description || '暂无描述'}</p>
<div className="project-meta">
<span>访问: {project.visit_count}</span>
</div>
</Card>
</Col>
))}
{projects.length === 0 && !loading && (
<Col span={24}>
<Empty description="还没有项目,创建一个开始吧" />
</Col>
)}
</Row>
<Modal
title="创建新项目"
open={modalVisible}
onCancel={() => {
setModalVisible(false)
form.resetFields()
}}
footer={null}
>
<Form
form={form}
layout="vertical"
onFinish={handleCreateProject}
>
<Form.Item
label="项目名称"
name="name"
rules={[{ required: true, message: '请输入项目名称' }]}
>
<Input placeholder="请输入项目名称" />
</Form.Item>
<Form.Item
label="项目描述"
name="description"
>
<Input.TextArea
rows={4}
placeholder="请输入项目描述(选填)"
/>
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
创建
</Button>
<Button onClick={() => {
setModalVisible(false)
form.resetFields()
}}>
取消
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
<Modal
title="分享项目"
open={shareModalVisible}
onCancel={() => setShareModalVisible(false)}
footer={null}
width={500}
>
{shareInfo && (
<Space direction="vertical" style={{ width: '100%' }} size="large">
<div>
<label style={{ marginBottom: 8, display: 'block', fontWeight: 500 }}>分享链接</label>
<Input
value={`${window.location.origin}${shareInfo.share_url}`}
readOnly
addonAfter={
<CopyOutlined onClick={handleCopyLink} style={{ cursor: 'pointer' }} />
}
/>
</div>
<div>
<Space>
<span style={{ fontWeight: 500 }}>访问密码保护</span>
<Switch checked={hasPassword} onChange={handlePasswordToggle} />
</Space>
</div>
{hasPassword && (
<div>
<Input.Password
placeholder="请输入访问密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Button
type="primary"
onClick={handleSavePassword}
style={{ marginTop: 8 }}
>
保存密码
</Button>
</div>
)}
</Space>
)}
</Modal>
</div>
</MainLayout>
)
}
export default ProjectList