feat(blog): 前端 posts API 封装 + 摘要/时长派生

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cc 2026-06-27 16:49:44 +08:00
parent edb975d818
commit d25c65ef81

92
Client/src/api/posts.ts Normal file
View File

@ -0,0 +1,92 @@
// 博客文章接口封装。基址与 photos.ts 一致:生产留空走同源相对路径。
const API_BASE =
(import.meta.env.VITE_API_BASE as string | undefined) ?? 'http://localhost:8080'
export interface Post {
id: string
title: string
tag: string
body: string
createdAt: number
updatedAt: number
}
export type PostInput = { title: string; tag: string; body: string }
/** 保存失败的可区分原因,供 UI 给出对应提示 */
export type PostError = 'full' | 'too-large' | 'bad-input' | 'failed'
function authHeaders(token: string): Record<string, string> {
return { Authorization: `Bearer ${token}` }
}
function saveError(status: number): PostError {
return status === 409
? 'full'
: status === 413
? 'too-large'
: status === 400
? 'bad-input'
: 'failed'
}
/** 列出当前用户全部文章(已按最新在前排序)与数量上限 */
export async function listPosts(token: string): Promise<{ items: Post[]; max: number }> {
const res = await fetch(`${API_BASE}/api/web/posts`, { headers: authHeaders(token) })
if (!res.ok) throw new Error(`list failed: ${res.status}`)
return (await res.json()) as { items: Post[]; max: number }
}
/** 取单篇文章 */
export async function getPost(token: string, id: string): Promise<Post> {
const res = await fetch(`${API_BASE}/api/web/posts/${id}`, { headers: authHeaders(token) })
if (!res.ok) throw new Error(`get failed: ${res.status}`)
return (await res.json()) as Post
}
/** 新建文章,返回服务器分配的完整文章对象 */
export async function createPost(token: string, input: PostInput): Promise<Post> {
const res = await fetch(`${API_BASE}/api/web/posts`, {
method: 'POST',
headers: { ...authHeaders(token), 'Content-Type': 'application/json' },
body: JSON.stringify(input),
})
if (res.ok) return (await res.json()) as Post
throw new Error(saveError(res.status))
}
/** 更新文章 */
export async function updatePost(token: string, id: string, input: PostInput): Promise<void> {
const res = await fetch(`${API_BASE}/api/web/posts/${id}`, {
method: 'PUT',
headers: { ...authHeaders(token), 'Content-Type': 'application/json' },
body: JSON.stringify(input),
})
if (!res.ok) throw new Error(saveError(res.status))
}
/** 删除文章404 视为已删,不报错) */
export async function deletePost(token: string, id: string): Promise<void> {
const res = await fetch(`${API_BASE}/api/web/posts/${id}`, {
method: 'DELETE',
headers: authHeaders(token),
})
if (!res.ok && res.status !== 404) throw new Error(`delete failed: ${res.status}`)
}
/** 估算阅读时长(按 ~200 字/分钟,最少 1 分钟) */
export function readingTime(body: string): string {
const mins = Math.max(1, Math.round(body.trim().length / 200))
return `${mins} min`
}
/** 取正文前若干字做摘要,去掉常见 Markdown 标记 */
export function excerpt(body: string, max = 80): string {
const plain = body
.replace(/```[\s\S]*?```/g, ' ') // 代码块
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1') // 链接/图片 → 文本
.replace(/[#>*_`~]+/g, ' ') // 标记符号
.replace(/\s+/g, ' ')
.trim()
return plain.length > max ? `${plain.slice(0, max)}` : plain
}