21 KiB
🚀 前端开发快速启动指南 - Day 18
日期: 2025-11-05 状态: ✅ 后端 API 就绪,前端可以立即开始开发 预计工作量: 16-22 小时(2-3 天)
📋 前提条件检查清单
在开始开发前,确保以下条件已满足:
-
后端 API 正在运行
# 如果未运行,执行: cd colaflow-api/src/ColaFlow.Api dotnet run -
可以访问 Scalar UI 打开浏览器:http://localhost:5167/scalar/v1
-
已阅读 API 文档 位置:
docs/api/FRONTEND_HANDOFF_DAY16.md -
前端项目可以运行
cd colaflow-web npm install npm run dev
🎯 Day 18 开发目标
核心目标: 完成 ProjectManagement API 集成,替换旧的 Issue Management API
必须完成的功能 (P0):
- ✅ Projects 列表和详情页面
- ✅ Epics 列表和详情页面
- ✅ Stories 列表和详情页面
- ✅ Tasks 列表和详情页面
- ✅ 更新 Kanban Board 使用新 API
可选功能 (P1):
- Sprint 管理基础功能
- User 管理界面
- SignalR 实时更新
🚀 快速开始(5分钟)
Step 1: 验证后端 API
打开浏览器访问:http://localhost:5167/scalar/v1
你应该看到 Scalar API 文档界面,包含以下模块:
- 🔐 Authentication
- 📦 ProjectManagement
- 👤 Identity & Tenants
- 📡 Real-time (SignalR)
Step 2: 测试 API(使用 Scalar UI)
- 点击 "Authorize" 按钮
- 获取 JWT token(从登录接口或使用测试 token)
- 输入:
Bearer <your-token> - 测试几个端点:
GET /api/v1/projects- 获取项目列表GET /api/v1/epics- 获取 Epic 列表
Step 3: 生成 TypeScript 类型(推荐)
cd colaflow-web
# 安装类型生成工具
npm install --save-dev openapi-typescript
# 生成类型
npx openapi-typescript http://localhost:5167/openapi/v1.json --output ./src/types/api.ts
# 查看生成的类型
cat src/types/api.ts | head -50
📚 关键文档位置
| 文档 | 位置 | 用途 |
|---|---|---|
| API 完整参考 | docs/api/ProjectManagement-API-Reference.md |
所有端点详细说明 |
| API 端点清单 | docs/api/API-Endpoints-Summary.md |
快速查找端点 |
| 前端集成指南 | docs/api/FRONTEND_HANDOFF_DAY16.md |
代码示例和最佳实践 |
| OpenAPI Spec | docs/api/openapi.json |
标准 OpenAPI 3.0 规范 |
| Scalar UI | http://localhost:5167/scalar/v1 | 交互式 API 文档 |
🔧 开发工作流
Phase 1: API Client 设置(1-2小时)
1.1 创建 API Client 基础配置
文件: colaflow-web/lib/api/client.ts
import axios, { AxiosInstance } from 'axios';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5167';
class ApiClient {
private client: AxiosInstance;
constructor() {
this.client = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器 - 添加 JWT token
this.client.interceptors.request.use((config) => {
const token = localStorage.getItem('jwt_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截器 - 处理错误
this.client.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Token 过期,跳转到登录页
localStorage.removeItem('jwt_token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
}
public get<T>(url: string, params?: any) {
return this.client.get<T>(url, { params });
}
public post<T>(url: string, data?: any) {
return this.client.post<T>(url, data);
}
public put<T>(url: string, data?: any) {
return this.client.put<T>(url, data);
}
public delete<T>(url: string) {
return this.client.delete<T>(url);
}
}
export const apiClient = new ApiClient();
1.2 创建 ProjectManagement API 模块
文件: colaflow-web/lib/api/pm.ts
import { apiClient } from './client';
// Types (可以从 openapi-typescript 生成的文件导入)
export interface Project {
id: string;
name: string;
key: string;
description?: string;
tenantId: string;
createdAt: string;
updatedAt: string;
}
export interface Epic {
id: string;
title: string;
description?: string;
projectId: string;
status: 'Backlog' | 'Todo' | 'InProgress' | 'Done';
priority: 'Low' | 'Medium' | 'High' | 'Critical';
estimatedHours?: number;
actualHours?: number;
assigneeId?: string;
tenantId: string;
createdAt: string;
updatedAt: string;
}
export interface Story {
id: string;
title: string;
description?: string;
epicId: string;
projectId: string;
status: 'Backlog' | 'Todo' | 'InProgress' | 'Done';
priority: 'Low' | 'Medium' | 'High' | 'Critical';
estimatedHours?: number;
actualHours?: number;
assigneeId?: string;
tenantId: string;
createdAt: string;
updatedAt: string;
}
export interface Task {
id: string;
title: string;
description?: string;
storyId: string;
projectId: string;
status: 'Backlog' | 'Todo' | 'InProgress' | 'Done';
priority: 'Low' | 'Medium' | 'High' | 'Critical';
estimatedHours?: number;
actualHours?: number;
assigneeId?: string;
tenantId: string;
createdAt: string;
updatedAt: string;
}
// API 方法
export const projectsApi = {
list: () => apiClient.get<Project[]>('/api/v1/projects'),
get: (id: string) => apiClient.get<Project>(`/api/v1/projects/${id}`),
create: (data: { name: string; key: string; description?: string }) =>
apiClient.post<Project>('/api/v1/projects', data),
update: (id: string, data: { name: string; key: string; description?: string }) =>
apiClient.put<Project>(`/api/v1/projects/${id}`, data),
delete: (id: string) => apiClient.delete(`/api/v1/projects/${id}`),
};
export const epicsApi = {
list: (projectId?: string) =>
apiClient.get<Epic[]>('/api/v1/epics', { projectId }),
get: (id: string) => apiClient.get<Epic>(`/api/v1/epics/${id}`),
create: (data: {
projectId: string;
title: string;
description?: string;
priority: Epic['priority'];
estimatedHours?: number;
}) => apiClient.post<Epic>('/api/v1/epics', data),
update: (id: string, data: Partial<Epic>) =>
apiClient.put<Epic>(`/api/v1/epics/${id}`, data),
changeStatus: (id: string, status: Epic['status']) =>
apiClient.put<Epic>(`/api/v1/epics/${id}/status`, { status }),
assign: (id: string, assigneeId: string) =>
apiClient.put<Epic>(`/api/v1/epics/${id}/assign`, { assigneeId }),
};
export const storiesApi = {
list: (epicId?: string) =>
apiClient.get<Story[]>('/api/v1/stories', { epicId }),
get: (id: string) => apiClient.get<Story>(`/api/v1/stories/${id}`),
create: (data: {
epicId: string;
title: string;
description?: string;
priority: Story['priority'];
estimatedHours?: number;
}) => apiClient.post<Story>('/api/v1/stories', data),
update: (id: string, data: Partial<Story>) =>
apiClient.put<Story>(`/api/v1/stories/${id}`, data),
assign: (id: string, assigneeId: string) =>
apiClient.put<Story>(`/api/v1/stories/${id}/assign`, { assigneeId }),
};
export const tasksApi = {
list: (storyId?: string) =>
apiClient.get<Task[]>('/api/v1/tasks', { storyId }),
get: (id: string) => apiClient.get<Task>(`/api/v1/tasks/${id}`),
create: (data: {
storyId: string;
title: string;
description?: string;
priority: Task['priority'];
estimatedHours?: number;
}) => apiClient.post<Task>('/api/v1/tasks', data),
update: (id: string, data: Partial<Task>) =>
apiClient.put<Task>(`/api/v1/tasks/${id}`, data),
changeStatus: (id: string, status: Task['status']) =>
apiClient.put<Task>(`/api/v1/tasks/${id}/status`, { status }),
assign: (id: string, assigneeId: string) =>
apiClient.put<Task>(`/api/v1/tasks/${id}/assign`, { assigneeId }),
};
1.3 创建 React Query Hooks
文件: colaflow-web/lib/hooks/use-projects.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { projectsApi, Project } from '@/lib/api/pm';
import { toast } from 'sonner';
export function useProjects() {
return useQuery({
queryKey: ['projects'],
queryFn: async () => {
const response = await projectsApi.list();
return response.data;
},
});
}
export function useProject(id: string) {
return useQuery({
queryKey: ['projects', id],
queryFn: async () => {
const response = await projectsApi.get(id);
return response.data;
},
enabled: !!id,
});
}
export function useCreateProject() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: { name: string; key: string; description?: string }) =>
projectsApi.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['projects'] });
toast.success('Project created successfully!');
},
onError: (error: any) => {
toast.error(error.response?.data?.detail || 'Failed to create project');
},
});
}
export function useUpdateProject() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<Project> }) =>
projectsApi.update(id, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['projects'] });
queryClient.invalidateQueries({ queryKey: ['projects', variables.id] });
toast.success('Project updated successfully!');
},
onError: (error: any) => {
toast.error(error.response?.data?.detail || 'Failed to update project');
},
});
}
export function useDeleteProject() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => projectsApi.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['projects'] });
toast.success('Project deleted successfully!');
},
onError: (error: any) => {
toast.error(error.response?.data?.detail || 'Failed to delete project');
},
});
}
类似地创建:
use-epics.tsuse-stories.tsuse-tasks.ts
Phase 2: Projects UI(3-4小时)
2.1 Projects 列表页面
文件: colaflow-web/app/(dashboard)/projects/page.tsx
'use client';
import { useProjects, useDeleteProject } from '@/lib/hooks/use-projects';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import Link from 'next/link';
import { PlusIcon, TrashIcon } from 'lucide-react';
export default function ProjectsPage() {
const { data: projects, isLoading, error } = useProjects();
const deleteProject = useDeleteProject();
if (isLoading) {
return (
<div className="space-y-4">
<Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
</div>
);
}
if (error) {
return (
<div className="text-center text-red-500">
Error loading projects: {error.message}
</div>
);
}
return (
<div className="container py-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold">Projects</h1>
<Link href="/projects/new">
<Button>
<PlusIcon className="mr-2 h-4 w-4" />
New Project
</Button>
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{projects?.map((project) => (
<Card key={project.id} className="p-6 hover:shadow-lg transition">
<Link href={`/projects/${project.id}`}>
<h3 className="text-xl font-semibold mb-2">{project.name}</h3>
<p className="text-sm text-muted-foreground mb-2">{project.key}</p>
{project.description && (
<p className="text-sm text-gray-600 mb-4">
{project.description}
</p>
)}
</Link>
<div className="flex justify-end">
<Button
variant="destructive"
size="sm"
onClick={() => {
if (confirm('Are you sure you want to delete this project?')) {
deleteProject.mutate(project.id);
}
}}
>
<TrashIcon className="h-4 w-4" />
</Button>
</div>
</Card>
))}
</div>
{projects?.length === 0 && (
<div className="text-center py-12">
<p className="text-muted-foreground">No projects yet. Create your first project!</p>
</div>
)}
</div>
);
}
2.2 Project 详情页面
文件: colaflow-web/app/(dashboard)/projects/[id]/page.tsx
'use client';
import { useProject } from '@/lib/hooks/use-projects';
import { useEpics } from '@/lib/hooks/use-epics';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import Link from 'next/link';
export default function ProjectDetailPage({ params }: { params: { id: string } }) {
const { data: project, isLoading: projectLoading } = useProject(params.id);
const { data: epics, isLoading: epicsLoading } = useEpics(params.id);
if (projectLoading) return <div>Loading project...</div>;
if (!project) return <div>Project not found</div>;
return (
<div className="container py-6">
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-3xl font-bold">{project.name}</h1>
<p className="text-muted-foreground">{project.key}</p>
</div>
<Button asChild>
<Link href={`/projects/${params.id}/epics/new`}>
New Epic
</Link>
</Button>
</div>
{project.description && (
<p className="mb-6 text-gray-600">{project.description}</p>
)}
<Tabs defaultValue="epics">
<TabsList>
<TabsTrigger value="epics">Epics</TabsTrigger>
<TabsTrigger value="kanban">Kanban Board</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="epics" className="mt-6">
{epicsLoading ? (
<div>Loading epics...</div>
) : (
<div className="space-y-4">
{epics?.map((epic) => (
<Card key={epic.id} className="p-4">
<Link href={`/projects/${params.id}/epics/${epic.id}`}>
<h3 className="font-semibold">{epic.title}</h3>
<p className="text-sm text-muted-foreground">{epic.description}</p>
<div className="flex gap-2 mt-2">
<Badge>{epic.status}</Badge>
<Badge variant="outline">{epic.priority}</Badge>
</div>
</Link>
</Card>
))}
</div>
)}
</TabsContent>
<TabsContent value="kanban">
<Link href={`/projects/${params.id}/kanban`}>
<Button>Open Kanban Board</Button>
</Link>
</TabsContent>
<TabsContent value="settings">
<div>Project settings coming soon...</div>
</TabsContent>
</Tabs>
</div>
);
}
Phase 3: Epics/Stories/Tasks UI(4-5小时)
按照类似的模式实现:
- Epic 列表和详情页
- Story 列表和详情页
- Task 列表和详情页
参考 Phase 2 的代码结构。
Phase 4: 更新 Kanban Board(5-6小时)
4.1 更新 Kanban Board 使用新 API
文件: colaflow-web/app/(dashboard)/projects/[id]/kanban/page.tsx
'use client';
import { useEpics } from '@/lib/hooks/use-epics';
import { useStories } from '@/lib/hooks/use-stories';
import { useTasks } from '@/lib/hooks/use-tasks';
import { KanbanBoard } from '@/components/kanban/KanbanBoard';
export default function KanbanPage({ params }: { params: { id: string } }) {
// 获取项目的所有 epics, stories, tasks
const { data: epics } = useEpics(params.id);
const { data: stories } = useStories(); // 可能需要按 project 过滤
const { data: tasks } = useTasks();
// 将数据转换为 Kanban Board 需要的格式
const kanbanData = useMemo(() => {
// 合并 epics, stories, tasks 到统一的工作项列表
const workItems = [
...(epics || []).map(epic => ({ ...epic, type: 'epic' as const })),
...(stories || []).map(story => ({ ...story, type: 'story' as const })),
...(tasks || []).map(task => ({ ...task, type: 'task' as const })),
];
// 按状态分组
return {
Backlog: workItems.filter(item => item.status === 'Backlog'),
Todo: workItems.filter(item => item.status === 'Todo'),
InProgress: workItems.filter(item => item.status === 'InProgress'),
Done: workItems.filter(item => item.status === 'Done'),
};
}, [epics, stories, tasks]);
return (
<div className="container py-6">
<h1 className="text-3xl font-bold mb-6">Kanban Board</h1>
<KanbanBoard data={kanbanData} projectId={params.id} />
</div>
);
}
🧪 测试清单
在提交代码前,请确保以下测试通过:
基础功能测试
- Projects 列表加载成功
- 创建新项目
- 编辑项目
- 删除项目
- 查看项目详情
Epics/Stories/Tasks 测试
- 创建 Epic
- 创建 Story(在 Epic 下)
- 创建 Task(在 Story 下)
- 更新状态(Backlog → Todo → InProgress → Done)
- 分配任务给用户
Kanban Board 测试
- 加载 Kanban Board
- 拖拽卡片更改状态
- 显示 Epic/Story/Task 层级关系
- 显示工时信息
错误处理测试
- 401 Unauthorized - 跳转到登录页
- 404 Not Found - 显示友好错误消息
- 网络错误 - 显示错误提示
🐛 常见问题与解决方案
问题 1: CORS 错误
症状: Access-Control-Allow-Origin 错误
解决方案:
// 确保 API 已配置 CORS(后端已配置)
// 前端无需额外处理
问题 2: 401 Unauthorized
症状: 所有请求返回 401
解决方案:
// 检查 JWT token 是否正确设置
const token = localStorage.getItem('jwt_token');
console.log('Token:', token);
// 检查 token 格式
// 应该是: Bearer <token>
问题 3: 404 Not Found(但资源存在)
症状: 可以看到资源,但 API 返回 404
原因: 多租户隔离 - 资源属于其他租户
解决方案:
// 确保 JWT token 包含正确的 tenant_id
// 检查 JWT payload:
const payload = JSON.parse(atob(token.split('.')[1]));
console.log('Tenant ID:', payload.tenant_id);
问题 4: TypeScript 类型错误
症状: Property 'xxx' does not exist on type
解决方案:
# 重新生成类型
npx openapi-typescript http://localhost:5167/openapi/v1.json --output ./src/types/api.ts
# 或者手动定义类型
# 参考 docs/api/ProjectManagement-API-Reference.md 中的 Data Models
📞 获取帮助
文档资源
- API 文档:
docs/api/ProjectManagement-API-Reference.md - Scalar UI: http://localhost:5167/scalar/v1
- 交接指南:
docs/api/FRONTEND_HANDOFF_DAY16.md
后端团队联系
- 如果遇到 API 问题,请查看后端日志
- 如果需要新的 API 端点,请联系后端团队
测试 Token
# 使用 Scalar UI 的 "Try It" 功能测试 API
# 或使用 curl:
curl -H "Authorization: Bearer <token>" http://localhost:5167/api/v1/projects
✅ 完成标准
Day 18 结束时,应该完成:
-
✅ API 集成
- Projects CRUD 完成
- Epics CRUD 完成
- Stories CRUD 完成
- Tasks CRUD 完成
-
✅ UI 实现
- 项目列表页
- 项目详情页
- Epic/Story/Task 列表页
- Kanban Board 更新
-
✅ 测试验证
- 所有基础功能测试通过
- 错误处理正确
- 多租户隔离验证
-
✅ 代码质量
- TypeScript 类型安全
- React Query 缓存优化
- 用户体验流畅
🎉 开始开发吧!
记住:
- 🚀 后端 API 已就绪(95% production ready)
- 📚 完整文档可用(6,000+ 行)
- 🛡️ 多租户安全已验证(100%)
- ✅ 所有测试通过(39/39)
你已经拥有了所有需要的资源,开始编码吧! 💪
Last Updated: 2025-11-05 (Day 16) Status: ✅ Frontend Development Ready Estimated Time: 16-22 hours (2-3 days)