Files
Rs_blog_front/docs/apidev_part2.md
2026-02-12 00:34:21 +08:00

552 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🔌 后端对接集成文档 Part 2 (API Integration Spec - Extended)
**延续文档**: `apidev_part1.md`
**覆盖功能**: 注册、文章更新/删除、认证管理、草稿流程
---
## 1. 补充数据模型 (Extended TypeScript Interfaces)
`src/types/api.ts` 中追加以下接口:
```typescript
// 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` 中追加以下方法:
```typescript
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 后跳转登录页 |
**组件实现建议:**
```typescript
// 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 -> 编辑页面
```
**数据加载流程:**
```typescript
// 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" |
**极简自动保存指示器:**
```typescript
// 不要用 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):**
```typescript
// 第一次点击:文字变化
// 第二次点击:执行删除
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:**
```typescript
// 极简 Toggle不用开关组件用文字按钮
<button
onClick={() => api.updatePost(id, { published: !isPublished })}
className="text-sm text-neutral-500 hover:text-black transition-colors"
>
{isPublished ? "Unpublish" : "Publish"}
</button>
```
**列表页草稿标识:**
```typescript
// 草稿文章标题后显示灰色 (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 存储策略
```typescript
// 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)
```typescript
// 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` 函数:
```typescript
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 的开发顺序:
6. **Step 6: Register Page** — 完成注册页面,复用登录页样式。
7. **Step 7: Edit Flow** — 完成文章编辑功能,实现 `PUT /posts/:id`
8. **Step 8: Delete Flow** — 完成删除功能,实现 Inline Confirmation 模式。
9. **Step 9: Draft Management** — 完成草稿列表页,支持发布/取消发布切换。
10. **Step 10: Auth Guard** — 添加全局认证守卫,保护需要登录的路由。
11. **Step 11: Polish** — 统一错误处理、Loading 状态、过渡动画。
---
## 8. 完整 API 客户端 (Final api.ts)
汇总 Part 1 + Part 2 的完整 API 客户端:
```typescript
// 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 规范中的所有端点。