297 lines
9.1 KiB
React
297 lines
9.1 KiB
React
|
|
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
|