diff --git a/apps/web/app/components/CreateTaskForm.tsx b/apps/web/app/components/CreateTaskForm.tsx
new file mode 100644
index 000000000..d130f9800
--- /dev/null
+++ b/apps/web/app/components/CreateTaskForm.tsx
@@ -0,0 +1,200 @@
+'use client';
+
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+
+const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
+
+interface CreateTaskFormProps {
+ projectId: string;
+ workspaceId: string;
+ onSuccess?: () => void;
+ onCancel?: () => void;
+}
+
+export default function CreateTaskForm({ projectId, workspaceId, onSuccess, onCancel }: CreateTaskFormProps) {
+ const router = useRouter();
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+ const [formData, setFormData] = useState({
+ name: '',
+ description: '',
+ priority: 'medium',
+ assignee_user_id: '',
+ parent_task_id: '',
+ });
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setLoading(true);
+ setError('');
+
+ try {
+ const res = await fetch(`${API_BASE}/api/v1/project-management/tasks`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ project_id: projectId,
+ workspace_id: workspaceId,
+ ...formData,
+ }),
+ });
+
+ if (!res.ok) {
+ const data = await res.json();
+ throw new Error(data.detail || '创建失败');
+ }
+
+ if (onSuccess) {
+ onSuccess();
+ } else {
+ router.push('/projects');
+ }
+ } catch (err: any) {
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/apps/web/app/projects/page.tsx b/apps/web/app/projects/page.tsx
index 2a3dddfe2..b83e73848 100644
--- a/apps/web/app/projects/page.tsx
+++ b/apps/web/app/projects/page.tsx
@@ -2,6 +2,7 @@
import { useEffect, useState } from 'react';
import Link from 'next/link';
+import CreateTaskForm from '../components/CreateTaskForm';
interface Task {
id: string;
@@ -20,15 +21,18 @@ export default function ProjectsPage() {
const [tasks, setTasks] = useState([]);
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('获取任务列表失败');
@@ -73,7 +77,7 @@ export default function ProjectsPage() {
return texts[priority] || priority;
};
- if (loading) {
+ if (loading && !showCreateForm) {
return (
加载中...
@@ -113,7 +117,7 @@ export default function ProjectsPage() {
刷新
@@ -144,7 +148,17 @@ export default function ProjectsPage() {
)}
- {tasks.length === 0 ? (
+ {showCreateForm ? (
+ {
+ setShowCreateForm(false);
+ fetchTasks();
+ }}
+ onCancel={() => setShowCreateForm(false)}
+ />
+ ) : tasks.length === 0 ? (
{tasks.map((task) => (
- | {task.name} |
+
+
+ {task.name}
+
+ |
(null);
+ const [issues, setIssues] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+ const [updating, setUpdating] = useState(false);
+
+ useEffect(() => {
+ fetchTaskDetail();
+ fetchTaskIssues();
+ }, [taskId]);
+
+ const fetchTaskDetail = async () => {
+ try {
+ // 因为没有单任务详情接口,这里演示如何处理
+ // 生产环境应该补一个 GET /tasks/{task_id} 接口
+ setError('任务详情接口待补充');
+ } catch (err: any) {
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchTaskIssues = async () => {
+ try {
+ const res = await fetch(`${API_BASE}/api/v1/project-management/issues?task_id=${taskId}`);
+ if (res.ok) {
+ const data = await res.json();
+ setIssues(data);
+ }
+ } catch (err) {
+ console.error('获取问题列表失败:', err);
+ }
+ };
+
+ const updateStatus = async (newStatus: string) => {
+ setUpdating(true);
+ try {
+ const res = await fetch(`${API_BASE}/api/v1/project-management/tasks/${taskId}/status`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ status: newStatus }),
+ });
+ if (!res.ok) throw new Error('更新状态失败');
+ await fetchTaskDetail();
+ } catch (err: any) {
+ alert(err.message);
+ } finally {
+ setUpdating(false);
+ }
+ };
+
+ const updateProgress = async (newProgress: number) => {
+ setUpdating(true);
+ try {
+ const res = await fetch(`${API_BASE}/api/v1/project-management/tasks/${taskId}/progress`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ progress: newProgress }),
+ });
+ if (!res.ok) throw new Error('更新进度失败');
+ await fetchTaskDetail();
+ } catch (err: any) {
+ alert(err.message);
+ } finally {
+ setUpdating(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+ {/* Main */}
+
+ {error && (
+
+ {error}
+
+ 提示:需要在后端补充 GET /api/v1/project-management/tasks/{'{task_id}'} 接口
+
+
+ )}
+
+ {task && (
+
+ {/* 任务基本信息 */}
+
+ {task.name}
+ {task.description || '暂无描述'}
+
+
+
+
+ 优先级
+ {task.priority}
+
+
+ 进度
+ {task.progress}%
+
+
+
+
+ {/* 问题卡点列表 */}
+
+ 问题卡点 ({issues.length})
+ {issues.length === 0 ? (
+ 暂无问题
+ ) : (
+
+ {issues.map(issue => (
+
+ {issue.resolved ? '🟢' : '🔴'}
+
+ {issue.title}
+ {issue.description && (
+
+ {issue.description}
+
+ )}
+
+
+ {issue.resolved ? '已解决' : '未解决'}
+
+
+ ))}
+
+ )}
+
+
+ )}
+
+
+ );
+}
|