243 lines
7.9 KiB
Markdown
243 lines
7.9 KiB
Markdown
太棒了。既然前端视觉风格(极简 + Inter + 锐利图片)已经确认,接下来我们进入 **逻辑层**。
|
||
|
||
这是第二份文档:**《Rust Backend API 集成指南 (Backend Integration Spec)》**。
|
||
|
||
这份文档将指导你如何将那个“冷峻”的界面与 Rust 后端进行优雅对接。我们不仅关注数据获取,更关注**数据如何驱动极简交互**(例如:如何用数据加载状态代替复杂的 Loading 动画)。
|
||
|
||
---
|
||
|
||
# 🔌 后端对接集成文档 (API Integration Spec)
|
||
|
||
**后端服务**: Rust Blog Backend
|
||
**Base URL**: `http://127.0.0.1:8765`
|
||
**协议**: REST / JSON
|
||
**认证**: Bearer Token
|
||
|
||
---
|
||
|
||
## 1. 数据模型定义 (TypeScript Interfaces)
|
||
|
||
为了确保前端代码的健壮性,我们需要根据 OpenAPI 文档定义严格的 TypeScript 接口。建议存放在 `src/types/api.ts`。
|
||
|
||
```typescript
|
||
// 基础响应包装(根据 Rust 后端习惯,通常可能直接返回数据或包裹在 data 字段)
|
||
// 针对此 API 文档的直接映射:
|
||
export interface ApiResponse<T> {
|
||
[key: string]: any;
|
||
// 注意:实际调试时请确认后端是否包裹了 { data: ... } 结构
|
||
}
|
||
|
||
// 1. 认证模型 (Auth)
|
||
export interface LoginRequest {
|
||
email: string;
|
||
password: string;
|
||
}
|
||
|
||
export interface AuthResponse {
|
||
token: string; // 假设后端返回 JWT 字段
|
||
user: {
|
||
id: string;
|
||
email: string;
|
||
};
|
||
}
|
||
|
||
// 2. 文章模型 (Post)
|
||
export interface Post {
|
||
id: string; // 对应 Rust 的 UUID
|
||
title: string; // 对应 OpenAPI schema
|
||
content: string; // Markdown 原始内容
|
||
published: boolean;
|
||
created_at?: string; // ISO 8601 Date String
|
||
cover_image?: string; // 文章封面图 URL (可选)
|
||
}
|
||
|
||
// 3. 分页参数 (Pagination)
|
||
export interface PageParams {
|
||
page: number; // e.g. 1
|
||
limit: number; // e.g. 10
|
||
}
|
||
|
||
```
|
||
|
||
---
|
||
|
||
## 2. API 客户端封装 (Service Layer)
|
||
|
||
为了保持组件的“极简”,不要在组件内部直接写 `fetch`。建议封装一个 `apiClient`。
|
||
|
||
**文件**: `src/lib/api.ts`
|
||
|
||
```typescript
|
||
const BASE_URL = 'http://127.0.0.1:8765/api';
|
||
|
||
// 极简的 Fetch 封装,自动处理 Token 和 错误
|
||
async function request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||
const token = localStorage.getItem('auth_token');
|
||
|
||
const headers = {
|
||
'Content-Type': 'application/json',
|
||
...(token && { 'Authorization': `Bearer ${token}` }),
|
||
...options.headers,
|
||
};
|
||
|
||
const response = await fetch(`${BASE_URL}${endpoint}`, {
|
||
...options,
|
||
headers,
|
||
});
|
||
|
||
// 极简错误处理:直接抛出状态码,由 UI 层决定是变红框还是显示线条
|
||
if (!response.ok) {
|
||
throw { status: response.status, message: response.statusText };
|
||
}
|
||
|
||
// 处理 204 No Content
|
||
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),
|
||
}),
|
||
|
||
// Posts
|
||
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}`),
|
||
|
||
createPost: (data: Partial<Post>) => request<Post>('/posts', {
|
||
method: 'POST',
|
||
body: JSON.stringify(data),
|
||
}),
|
||
|
||
// Upload (特殊处理 Multipart)
|
||
uploadImage: async (file: File) => {
|
||
const token = localStorage.getItem('auth_token');
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
const response = await fetch(`${BASE_URL}/upload`, {
|
||
method: 'POST',
|
||
headers: { 'Authorization': `Bearer ${token}` }, // 不设置 Content-Type,让浏览器自动设置 boundary
|
||
body: formData
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Upload failed');
|
||
return response.json(); // 返回 { url: "..." }
|
||
}
|
||
};
|
||
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 功能模块集成策略
|
||
|
||
### 3.1 首页文章列表 (Infinite Scroll / Pagination)
|
||
|
||
**极简主义的数据加载不应有“旋转的菊花”**。
|
||
|
||
* **加载状态 (Loading)**: 使用**骨架屏 (Skeleton)**。
|
||
* 在数据回来之前,渲染 3 个灰色的方块 (`bg-neutral-100`),位置与图片和标题完全重合。这能保持页面的“稳定感”,不会因为数据加载而发生布局跳动。
|
||
|
||
|
||
* **API**: `GET /api/posts?page=1&limit=5`
|
||
* **错误状态**: 如果 API 挂了,不要弹窗。在列表顶部显示一行极小的灰色文字:*"Could not retrieve latest transmission."*
|
||
|
||
### 3.2 文章详情与 Markdown 渲染
|
||
|
||
技术博客的核心是代码阅读体验。
|
||
|
||
* **API**: `GET /api/posts/{id}`
|
||
* **Markdown 解析**: 推荐使用 `react-markdown`。
|
||
* **代码高亮**: 使用 `rehype-highlight` 或 `prismjs`。
|
||
* **样式定制**:
|
||
* 为了符合极简风格,必须重写 `<pre>` 和 `<code>` 的样式。
|
||
* **代码块背景**: `#fafafa` (极淡灰)。
|
||
* **字体**: `JetBrains Mono`。
|
||
* **边框**: 无边框,直角。
|
||
|
||
|
||
|
||
```typescript
|
||
// Markdown 组件配置示例
|
||
<ReactMarkdown
|
||
rehypePlugins={[rehypeHighlight]}
|
||
components={{
|
||
// 覆盖默认图片样式,确保圆角为 0
|
||
img: ({node, ...props}) => (
|
||
<img {...props} className="w-full h-auto aspect-video object-cover my-8 border border-neutral-100" />
|
||
),
|
||
// 覆盖代码块
|
||
code: ({node, inline, className, children, ...props}) => {
|
||
return !inline ? (
|
||
<pre className="bg-neutral-50 p-6 overflow-x-auto text-sm my-8">
|
||
<code className={className} {...props}>{children}</code>
|
||
</pre>
|
||
) : (
|
||
<code className="bg-neutral-100 px-1 py-0.5 text-sm font-mono text-red-600" {...props}>{children}</code>
|
||
)
|
||
}
|
||
}}
|
||
>
|
||
{post.content}
|
||
</ReactMarkdown>
|
||
|
||
```
|
||
|
||
### 3.3 管理员发布文章 (Writer Mode)
|
||
|
||
这是一个关键的交互点。我们需要一个**所见即所得 (WYSIWYG) 但又极简**的编辑器。
|
||
|
||
* **布局**: 左侧 Markdown 输入框 (Monospace),右侧实时预览。
|
||
* **图片上传集成**:
|
||
1. 用户将截图直接拖入 Markdown 输入框。
|
||
2. 触发 `onDrop` 事件 -> 调用 `api.uploadImage(file)`。
|
||
3. 上传中:光标处显示 `![Uploading...]` (灰色)。
|
||
4. 上传完成:替换为 ``。
|
||
|
||
|
||
* **API 调用**:
|
||
* 点击 "Publish" -> `POST /api/posts` -> 成功后跳转至详情页。
|
||
* 验证错误 (400 Empty Title) -> 标题输入框下方的黑线变红 (`border-red-600`),不显示文字提示,或者仅显示极小的 "Required"。
|
||
|
||
|
||
|
||
---
|
||
|
||
## 4. 状态反馈映射表 (UI Feedback)
|
||
|
||
在极简设计中,我们利用**微交互**来反馈后端状态,而非模态框。
|
||
|
||
| 后端状态 (Status) | 场景 (Context) | 前端极简反馈 (Visual Feedback) |
|
||
| --- | --- | --- |
|
||
| **200 OK** | 登录成功 | 按钮文字从 "Enter" 变为 "Welcome",淡出并跳转。 |
|
||
| **200 OK** | 文章发布成功 | 页面右上角显示 "Saved." (纯文本,3秒消失)。 |
|
||
| **401 Unauthorized** | Token 过期 | 自动静默跳转至 `/login`,无提示。 |
|
||
| **400 Bad Request** | 密码太短 | 密码框下边框变红,同时有一个极轻微的左右震动动画 (Shake)。 |
|
||
| **404 Not Found** | 文章 ID 错误 | 页面中心显示巨大的 "404",配文 "Signal Lost"。 |
|
||
| **500 Server Error** | 服务器崩溃 | 页面顶部出现 2px 高的红色静止线条。 |
|
||
|
||
---
|
||
|
||
## 5. 开发路线图 (Roadmap)
|
||
|
||
建议按以下顺序开发,以最快速度跑通 MVP:
|
||
|
||
1. **Step 1: API Client**: 搭建 `api.ts`,跑通 `fetch` 封装。
|
||
2. **Step 2: Read-Only**: 完成首页列表 (`GET /posts`) 和详情页 (`GET /posts/:id`)。此时数据可以是 Mock 的。
|
||
3. **Step 3: Markdown**: 调优 `react-markdown` 样式,确保代码块和图片符合极简规范。
|
||
4. **Step 4: Auth**: 完成极简登录页 (`POST /login`)。
|
||
5. **Step 5: Editor**: 完成发布功能和图片上传 (`POST /upload` + Drag & Drop)。
|
||
|
||
准备好开始编码了吗?建议从配置 `api.ts` 和 `tailwind.config.js` 开始。 |