769 lines
21 KiB
Markdown
769 lines
21 KiB
Markdown
# 🚀 前端开发快速启动指南 - Day 18
|
||
|
||
**日期**: 2025-11-05
|
||
**状态**: ✅ 后端 API 就绪,前端可以立即开始开发
|
||
**预计工作量**: 16-22 小时(2-3 天)
|
||
|
||
---
|
||
|
||
## 📋 前提条件检查清单
|
||
|
||
在开始开发前,确保以下条件已满足:
|
||
|
||
- [ ] **后端 API 正在运行**
|
||
```bash
|
||
# 如果未运行,执行:
|
||
cd colaflow-api/src/ColaFlow.Api
|
||
dotnet run
|
||
```
|
||
|
||
- [ ] **可以访问 Scalar UI**
|
||
打开浏览器:http://localhost:5167/scalar/v1
|
||
|
||
- [ ] **已阅读 API 文档**
|
||
位置:`docs/api/FRONTEND_HANDOFF_DAY16.md`
|
||
|
||
- [ ] **前端项目可以运行**
|
||
```bash
|
||
cd colaflow-web
|
||
npm install
|
||
npm run dev
|
||
```
|
||
|
||
---
|
||
|
||
## 🎯 Day 18 开发目标
|
||
|
||
**核心目标**: 完成 ProjectManagement API 集成,替换旧的 Issue Management API
|
||
|
||
**必须完成的功能** (P0):
|
||
1. ✅ Projects 列表和详情页面
|
||
2. ✅ Epics 列表和详情页面
|
||
3. ✅ Stories 列表和详情页面
|
||
4. ✅ Tasks 列表和详情页面
|
||
5. ✅ 更新 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)
|
||
|
||
1. 点击 **"Authorize"** 按钮
|
||
2. 获取 JWT token(从登录接口或使用测试 token)
|
||
3. 输入:`Bearer <your-token>`
|
||
4. 测试几个端点:
|
||
- `GET /api/v1/projects` - 获取项目列表
|
||
- `GET /api/v1/epics` - 获取 Epic 列表
|
||
|
||
### Step 3: 生成 TypeScript 类型(推荐)
|
||
|
||
```bash
|
||
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`
|
||
|
||
```typescript
|
||
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`
|
||
|
||
```typescript
|
||
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`
|
||
|
||
```typescript
|
||
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.ts`
|
||
- `use-stories.ts`
|
||
- `use-tasks.ts`
|
||
|
||
---
|
||
|
||
### Phase 2: Projects UI(3-4小时)
|
||
|
||
#### 2.1 Projects 列表页面
|
||
|
||
**文件**: `colaflow-web/app/(dashboard)/projects/page.tsx`
|
||
|
||
```typescript
|
||
'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`
|
||
|
||
```typescript
|
||
'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`
|
||
|
||
```typescript
|
||
'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` 错误
|
||
|
||
**解决方案**:
|
||
```typescript
|
||
// 确保 API 已配置 CORS(后端已配置)
|
||
// 前端无需额外处理
|
||
```
|
||
|
||
### 问题 2: 401 Unauthorized
|
||
|
||
**症状**: 所有请求返回 401
|
||
|
||
**解决方案**:
|
||
```typescript
|
||
// 检查 JWT token 是否正确设置
|
||
const token = localStorage.getItem('jwt_token');
|
||
console.log('Token:', token);
|
||
|
||
// 检查 token 格式
|
||
// 应该是: Bearer <token>
|
||
```
|
||
|
||
### 问题 3: 404 Not Found(但资源存在)
|
||
|
||
**症状**: 可以看到资源,但 API 返回 404
|
||
|
||
**原因**: 多租户隔离 - 资源属于其他租户
|
||
|
||
**解决方案**:
|
||
```typescript
|
||
// 确保 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`
|
||
|
||
**解决方案**:
|
||
```bash
|
||
# 重新生成类型
|
||
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 结束时,应该完成:
|
||
|
||
1. ✅ **API 集成**
|
||
- Projects CRUD 完成
|
||
- Epics CRUD 完成
|
||
- Stories CRUD 完成
|
||
- Tasks CRUD 完成
|
||
|
||
2. ✅ **UI 实现**
|
||
- 项目列表页
|
||
- 项目详情页
|
||
- Epic/Story/Task 列表页
|
||
- Kanban Board 更新
|
||
|
||
3. ✅ **测试验证**
|
||
- 所有基础功能测试通过
|
||
- 错误处理正确
|
||
- 多租户隔离验证
|
||
|
||
4. ✅ **代码质量**
|
||
- 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)
|