bda4db5e5b
- CreateTaskForm component with validation - Task detail page with issue list - Integrated create form into projects page - Task name links to detail page - Added workspace_id parameter - Improved state management
262 lines
8.8 KiB
TypeScript
262 lines
8.8 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import Link from 'next/link';
|
|
import CreateTaskForm from '../components/CreateTaskForm';
|
|
|
|
interface Task {
|
|
id: string;
|
|
name: string;
|
|
status: string;
|
|
priority: string;
|
|
progress: number;
|
|
assignee_user_id: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
|
|
|
export default function ProjectsPage() {
|
|
const [tasks, setTasks] = useState<Task[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
const [showCreateForm, setShowCreateForm] = useState(false);
|
|
|
|
// 模拟项目ID,生产环境应该从路由或上下文获取
|
|
const projectId = 'demo_project_1';
|
|
const workspaceId = 'demo_workspace_1';
|
|
|
|
useEffect(() => {
|
|
fetchTasks();
|
|
}, []);
|
|
|
|
const fetchTasks = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch(`${API_BASE}/api/v1/project-management/tasks?project_id=${projectId}`);
|
|
if (!res.ok) throw new Error('获取任务列表失败');
|
|
const data = await res.json();
|
|
setTasks(data);
|
|
} catch (err: any) {
|
|
setError(err.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const getStatusColor = (status: string) => {
|
|
const colors: Record<string, string> = {
|
|
pending: '#86909C',
|
|
in_progress: '#165DFF',
|
|
completed: '#00B42A',
|
|
blocked: '#F53F3F',
|
|
cancelled: '#6E7681',
|
|
};
|
|
return colors[status] || '#6E7681';
|
|
};
|
|
|
|
const getStatusText = (status: string) => {
|
|
const texts: Record<string, string> = {
|
|
pending: '待开始',
|
|
in_progress: '进行中',
|
|
completed: '已完成',
|
|
blocked: '阻塞',
|
|
cancelled: '已取消',
|
|
};
|
|
return texts[status] || status;
|
|
};
|
|
|
|
const getPriorityText = (priority: string) => {
|
|
const texts: Record<string, string> = {
|
|
low: '低',
|
|
medium: '中',
|
|
high: '高',
|
|
urgent: '紧急',
|
|
};
|
|
return texts[priority] || priority;
|
|
};
|
|
|
|
if (loading && !showCreateForm) {
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh' }}>
|
|
<p style={{ color: 'var(--text-secondary)' }}>加载中...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
|
|
{/* Header */}
|
|
<header style={{
|
|
height: '60px',
|
|
background: 'var(--bg-white)',
|
|
borderBottom: '1px solid var(--border)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
padding: '0 20px',
|
|
justifyContent: 'space-between',
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '20px' }}>
|
|
<Link href="/" style={{ fontSize: '18px', fontWeight: 'bold', color: 'var(--primary)', textDecoration: 'none' }}>
|
|
📁 项目推进器
|
|
</Link>
|
|
<span style={{ color: 'var(--text-secondary)' }}>Demo 项目</span>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: '10px' }}>
|
|
<button
|
|
onClick={fetchTasks}
|
|
style={{
|
|
padding: '6px 12px',
|
|
borderRadius: '4px',
|
|
border: '1px solid var(--border)',
|
|
background: 'var(--bg-white)',
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
刷新
|
|
</button>
|
|
<button
|
|
onClick={() => setShowCreateForm(!showCreateForm)}
|
|
style={{
|
|
padding: '6px 12px',
|
|
borderRadius: '4px',
|
|
border: 'none',
|
|
background: 'var(--primary)',
|
|
color: 'white',
|
|
cursor: 'pointer',
|
|
fontWeight: 'bold',
|
|
}}
|
|
>
|
|
{showCreateForm ? '取消' : '+ 新增任务'}
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main Content */}
|
|
<main style={{ flex: 1, padding: '20px', overflow: 'auto' }}>
|
|
{error && (
|
|
<div style={{
|
|
padding: '12px',
|
|
background: '#FFECE8',
|
|
border: '1px solid #F53F3F',
|
|
borderRadius: '4px',
|
|
color: '#F53F3F',
|
|
marginBottom: '20px',
|
|
}}>
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{showCreateForm ? (
|
|
<CreateTaskForm
|
|
projectId={projectId}
|
|
workspaceId={workspaceId}
|
|
onSuccess={() => {
|
|
setShowCreateForm(false);
|
|
fetchTasks();
|
|
}}
|
|
onCancel={() => setShowCreateForm(false)}
|
|
/>
|
|
) : tasks.length === 0 ? (
|
|
<div style={{
|
|
background: 'var(--bg-white)',
|
|
borderRadius: '8px',
|
|
padding: '40px',
|
|
textAlign: 'center',
|
|
color: 'var(--text-secondary)',
|
|
}}>
|
|
<p>暂无任务</p>
|
|
<p style={{ fontSize: '14px', marginTop: '8px' }}>点击右上角"+ 新增任务"创建第一个任务</p>
|
|
</div>
|
|
) : (
|
|
<div style={{
|
|
background: 'var(--bg-white)',
|
|
borderRadius: '8px',
|
|
padding: '20px',
|
|
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
|
|
}}>
|
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
<thead>
|
|
<tr style={{ borderBottom: '1px solid var(--border)' }}>
|
|
<th style={{ padding: '12px', textAlign: 'left', color: 'var(--text-secondary)', fontWeight: 'normal' }}>任务名称</th>
|
|
<th style={{ padding: '12px', textAlign: 'left', color: 'var(--text-secondary)', fontWeight: 'normal' }}>状态</th>
|
|
<th style={{ padding: '12px', textAlign: 'left', color: 'var(--text-secondary)', fontWeight: 'normal' }}>优先级</th>
|
|
<th style={{ padding: '12px', textAlign: 'left', color: 'var(--text-secondary)', fontWeight: 'normal' }}>进度</th>
|
|
<th style={{ padding: '12px', textAlign: 'left', color: 'var(--text-secondary)', fontWeight: 'normal' }}>创建时间</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{tasks.map((task) => (
|
|
<tr key={task.id} style={{ borderBottom: '1px solid #F7F8FA' }}>
|
|
<td style={{ padding: '12px', fontWeight: '500' }}>
|
|
<Link href={`/tasks/${task.id}`} style={{ color: 'var(--primary)', textDecoration: 'none' }}>
|
|
{task.name}
|
|
</Link>
|
|
</td>
|
|
<td style={{ padding: '12px' }}>
|
|
<span style={{
|
|
display: 'inline-block',
|
|
padding: '2px 8px',
|
|
borderRadius: '4px',
|
|
fontSize: '12px',
|
|
color: 'white',
|
|
background: getStatusColor(task.status),
|
|
}}>
|
|
{getStatusText(task.status)}
|
|
</span>
|
|
</td>
|
|
<td style={{ padding: '12px', color: 'var(--text-secondary)' }}>
|
|
{getPriorityText(task.priority)}
|
|
</td>
|
|
<td style={{ padding: '12px' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
<div style={{
|
|
flex: 1,
|
|
height: '6px',
|
|
background: '#E5E6EB',
|
|
borderRadius: '3px',
|
|
overflow: 'hidden',
|
|
}}>
|
|
<div style={{
|
|
width: `${task.progress}%`,
|
|
height: '100%',
|
|
background: 'var(--primary)',
|
|
transition: 'width 0.3s',
|
|
}} />
|
|
</div>
|
|
<span style={{ fontSize: '12px', color: 'var(--text-secondary)', minWidth: '40px' }}>
|
|
{task.progress}%
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td style={{ padding: '12px', fontSize: '12px', color: 'var(--text-secondary)' }}>
|
|
{new Date(task.created_at).toLocaleDateString('zh-CN')}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</main>
|
|
|
|
{/* Footer */}
|
|
<footer style={{
|
|
height: '30px',
|
|
lineHeight: '30px',
|
|
background: 'var(--bg-white)',
|
|
borderTop: '1px solid var(--border)',
|
|
padding: '0 20px',
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
fontSize: '12px',
|
|
color: 'var(--text-secondary)',
|
|
}}>
|
|
<div>当前项目:Demo 项目</div>
|
|
<div>总计任务:{tasks.length}</div>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|