forked from carrydela/Rs_blog_front
13 KiB
13 KiB
🔌 后端对接集成文档 Part 2 (API Integration Spec - Extended)
延续文档: apidev_part1.md
覆盖功能: 注册、文章更新/删除、认证管理、草稿流程
1. 补充数据模型 (Extended TypeScript Interfaces)
在 src/types/api.ts 中追加以下接口:
// 1. 注册模型 (Registration)
export interface RegisterRequest {
email: string;
password: string; // 最小 8 字符
}
export interface RegisterResponse {
message: string;
user: {
id: string;
email: string;
};
}
// 2. 文章更新模型 (Post Update)
export interface UpdatePostRequest {
title?: string;
content?: string;
published?: boolean;
}
// 3. 删除响应 (Delete Response)
export interface DeleteResponse {
message: string;
deleted_at?: string; // ISO 8601 - 软删除时间戳
}
// 4. 分页响应包装 (Paginated Response)
export interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
limit: number;
total: number;
total_pages: number;
};
}
// 5. API 错误模型 (API Error)
export interface ApiError {
status: number;
message: string;
field?: string; // 验证错误时标识具体字段
}
2. API 客户端扩展 (Extended Service Layer)
在 src/lib/api.ts 中追加以下方法:
export const api = {
// === 原有方法保持不变 ===
// Auth - 新增注册
register: (data: RegisterRequest) => request<RegisterResponse>('/auth/register', {
method: 'POST',
body: JSON.stringify(data),
}),
// Auth - 登出 (清理本地 Token)
logout: () => {
localStorage.removeItem('auth_token');
localStorage.removeItem('user_info');
// 无需调用后端,JWT 无状态
},
// Posts - 更新文章
updatePost: (id: string, data: UpdatePostRequest) => request<Post>(`/posts/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
// Posts - 删除文章 (软删除)
deletePost: (id: string) => request<DeleteResponse>(`/posts/${id}`, {
method: 'DELETE',
}),
// Posts - 发布/取消发布 (便捷方法)
publishPost: (id: string) => request<Post>(`/posts/${id}`, {
method: 'PUT',
body: JSON.stringify({ published: true }),
}),
unpublishPost: (id: string) => request<Post>(`/posts/${id}`, {
method: 'PUT',
body: JSON.stringify({ published: false }),
}),
};
3. 功能模块集成策略 (续)
3.4 用户注册 (Registration Flow)
极简注册表单应与登录共享视觉语言。
- API:
POST /api/auth/register - 必填字段:
email,password - 验证规则:
- Email: 有效邮箱格式
- Password: 最少 8 字符
UI 交互映射:
| 用户行为 | 前端反馈 |
|---|---|
| 输入密码 < 8 字符 | 密码框下边框变灰 border-neutral-300,显示字符计数 5/8 |
| 提交时密码过短 | 边框变红 + 微震动,显示 "Min 8 characters" |
| Email 已存在 (409) | Email 框下方显示 "Already registered" |
| 注册成功 | 按钮文字变为 "Created",1.5s 后跳转登录页 |
组件实现建议:
// src/components/RegisterForm.tsx
const [charCount, setCharCount] = useState(0);
<input
type="password"
onChange={(e) => setCharCount(e.target.value.length)}
className={cn(
"border-b-2 transition-colors",
charCount < 8 && charCount > 0 ? "border-neutral-300" : "border-black"
)}
/>
{charCount > 0 && charCount < 8 && (
<span className="text-xs text-neutral-400 mt-1">{charCount}/8</span>
)}
3.5 文章编辑与更新 (Edit Post Flow)
编辑器应复用发布页面,但预填充现有数据。
- API:
PUT /api/posts/{id} - 可更新字段:
title,content,published - 权限: 需要 Bearer Token + 文章所有权
页面路由建议:
/posts/[id]/edit -> 编辑页面
数据加载流程:
// src/app/posts/[id]/edit/page.tsx
useEffect(() => {
const loadPost = async () => {
try {
const post = await api.getPostById(id);
setTitle(post.title);
setContent(post.content);
setPublished(post.published);
} catch (err) {
if (err.status === 404) router.push('/404');
if (err.status === 401) router.push('/login');
}
};
loadPost();
}, [id]);
保存策略:
| 策略 | 实现方式 |
|---|---|
| 手动保存 | 点击 "Save" 按钮触发 PUT |
| 自动保存 (可选) | debounce 300ms 后自动调用 updatePost,右上角显示 "Saving..." → "Saved" |
极简自动保存指示器:
// 不要用 spinner,用文字状态
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved'>('idle');
{saveStatus === 'saving' && <span className="text-neutral-400 text-xs">Saving...</span>}
{saveStatus === 'saved' && <span className="text-neutral-600 text-xs">Saved</span>}
3.6 文章删除 (Delete Post Flow)
删除是破坏性操作,但极简设计不使用模态确认框。
- API:
DELETE /api/posts/{id} - 行为: 软删除 (数据库标记
deleted_at) - 权限: 需要 Bearer Token + 文章所有权
极简确认模式 (Inline Confirmation):
// 第一次点击:文字变化
// 第二次点击:执行删除
const [confirmDelete, setConfirmDelete] = useState(false);
<button
onClick={() => {
if (confirmDelete) {
handleDelete();
} else {
setConfirmDelete(true);
setTimeout(() => setConfirmDelete(false), 3000); // 3秒后重置
}
}}
className={cn(
"text-sm transition-colors",
confirmDelete ? "text-red-600" : "text-neutral-400"
)}
>
{confirmDelete ? "Confirm delete" : "Delete"}
</button>
删除后行为:
| 场景 | 反馈 |
|---|---|
| 在列表页删除 | 该行淡出 (opacity-0 transition-opacity),然后移除 |
| 在详情页删除 | 显示 "Deleted",2秒后跳转首页 |
3.7 草稿与发布状态管理 (Draft/Published Toggle)
文章有两种状态:草稿 (draft) 和已发布 (published)。
- 草稿:
published: false— 仅作者可见 - 已发布:
published: true— 公开可见
状态切换 UI:
// 极简 Toggle:不用开关组件,用文字按钮
<button
onClick={() => api.updatePost(id, { published: !isPublished })}
className="text-sm text-neutral-500 hover:text-black transition-colors"
>
{isPublished ? "Unpublish" : "Publish"}
</button>
列表页草稿标识:
// 草稿文章标题后显示灰色 (Draft) 标记
<h2 className="text-lg font-medium">
{post.title}
{!post.published && (
<span className="text-neutral-400 text-sm ml-2">(Draft)</span>
)}
</h2>
4. 认证状态管理 (Auth State Management)
4.1 Token 存储策略
// src/lib/auth.ts
export const auth = {
// 存储 Token
setToken: (token: string) => {
localStorage.setItem('auth_token', token);
},
// 获取 Token
getToken: () => localStorage.getItem('auth_token'),
// 检查是否已登录
isAuthenticated: () => !!localStorage.getItem('auth_token'),
// 清除认证信息
clear: () => {
localStorage.removeItem('auth_token');
localStorage.removeItem('user_info');
},
// 存储用户信息
setUser: (user: { id: string; email: string }) => {
localStorage.setItem('user_info', JSON.stringify(user));
},
// 获取用户信息
getUser: () => {
const stored = localStorage.getItem('user_info');
return stored ? JSON.parse(stored) : null;
},
};
4.2 全局认证守卫 (Auth Guard)
// src/components/AuthGuard.tsx
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { auth } from '@/lib/auth';
export function AuthGuard({ children }: { children: React.ReactNode }) {
const router = useRouter();
useEffect(() => {
if (!auth.isAuthenticated()) {
router.replace('/login');
}
}, [router]);
if (!auth.isAuthenticated()) {
return null; // 或返回骨架屏
}
return <>{children}</>;
}
// 使用方式:包裹需要认证的页面
// src/app/posts/new/page.tsx
export default function NewPostPage() {
return (
<AuthGuard>
<PostEditor />
</AuthGuard>
);
}
4.3 401 全局拦截
更新 src/lib/api.ts 中的 request 函数:
async function request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
// ... 原有代码 ...
if (!response.ok) {
// 401 时自动清除 Token 并跳转
if (response.status === 401) {
auth.clear();
// 使用 window.location 而非 router,确保完全刷新状态
window.location.href = '/login';
}
throw { status: response.status, message: response.statusText };
}
// ... 原有代码 ...
}
5. 扩展状态反馈映射表 (Extended UI Feedback)
| 后端状态 | 场景 | 前端极简反馈 |
|---|---|---|
| 201 Created | 注册成功 | 按钮文字变为 "Created",跳转登录页 |
| 409 Conflict | Email 已存在 | Email 框下方显示 "Already registered" |
| 400 Bad Request | 密码过短 | 显示字符计数 5/8,边框变红 |
| 200 OK | 文章更新成功 | 右上角显示 "Saved" (2秒消失) |
| 200 OK | 文章删除成功 | 当前行/页面淡出,跳转 |
| 403 Forbidden | 无权编辑他人文章 | 显示 "Not your post",禁用编辑按钮 |
| 422 Unprocessable | 文章内容不合法 | 对应字段边框变红 |
6. 页面路由规划 (Route Structure)
/ # 首页 - 文章列表
/login # 登录
/register # 注册 (新增)
/posts/[id] # 文章详情
/posts/[id]/edit # 编辑文章 (新增,需认证)
/posts/new # 发布新文章 (需认证)
/drafts # 我的草稿列表 (新增,需认证)
/404 # 404 页面
7. 开发路线图 (续)
接续 Part 1 的开发顺序:
- Step 6: Register Page — 完成注册页面,复用登录页样式。
- Step 7: Edit Flow — 完成文章编辑功能,实现
PUT /posts/:id。 - Step 8: Delete Flow — 完成删除功能,实现 Inline Confirmation 模式。
- Step 9: Draft Management — 完成草稿列表页,支持发布/取消发布切换。
- Step 10: Auth Guard — 添加全局认证守卫,保护需要登录的路由。
- Step 11: Polish — 统一错误处理、Loading 状态、过渡动画。
8. 完整 API 客户端 (Final api.ts)
汇总 Part 1 + Part 2 的完整 API 客户端:
// src/lib/api.ts
import { auth } from './auth';
import type {
LoginRequest,
RegisterRequest,
AuthResponse,
RegisterResponse,
Post,
UpdatePostRequest,
DeleteResponse,
PageParams,
} from '@/types/api';
const BASE_URL = 'http://127.0.0.1:8765/api';
async function request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const token = auth.getToken();
const headers: HeadersInit = {
'Content-Type': 'application/json',
...(token && { 'Authorization': `Bearer ${token}` }),
...options.headers,
};
const response = await fetch(`${BASE_URL}${endpoint}`, {
...options,
headers,
});
if (!response.ok) {
if (response.status === 401) {
auth.clear();
window.location.href = '/login';
}
const errorBody = await response.json().catch(() => ({}));
throw {
status: response.status,
message: response.statusText,
...errorBody
};
}
if (response.status === 204) return {} as T;
return response.json();
}
export const api = {
// ===== Auth =====
login: (data: LoginRequest) => request<AuthResponse>('/auth/login', {
method: 'POST',
body: JSON.stringify(data),
}),
register: (data: RegisterRequest) => request<RegisterResponse>('/auth/register', {
method: 'POST',
body: JSON.stringify(data),
}),
logout: () => auth.clear(),
// ===== Posts - Read =====
getPosts: (params: PageParams) => {
const query = new URLSearchParams({
page: params.page.toString(),
limit: params.limit.toString()
});
return request<Post[]>(`/posts?${query}`);
},
getPostById: (id: string) => request<Post>(`/posts/${id}`),
// ===== Posts - Write =====
createPost: (data: Partial<Post>) => request<Post>('/posts', {
method: 'POST',
body: JSON.stringify(data),
}),
updatePost: (id: string, data: UpdatePostRequest) => request<Post>(`/posts/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
deletePost: (id: string) => request<DeleteResponse>(`/posts/${id}`, {
method: 'DELETE',
}),
// ===== Posts - Convenience =====
publishPost: (id: string) => request<Post>(`/posts/${id}`, {
method: 'PUT',
body: JSON.stringify({ published: true }),
}),
unpublishPost: (id: string) => request<Post>(`/posts/${id}`, {
method: 'PUT',
body: JSON.stringify({ published: false }),
}),
// ===== Upload =====
uploadImage: async (file: File) => {
const token = auth.getToken();
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${BASE_URL}/upload`, {
method: 'POST',
headers: {
...(token && { 'Authorization': `Bearer ${token}` })
},
body: formData
});
if (!response.ok) {
if (response.status === 401) {
auth.clear();
window.location.href = '/login';
}
throw new Error('Upload failed');
}
return response.json();
}
};
现在 Part 1 + Part 2 完整覆盖了你的 OpenAPI 规范中的所有端点。