diff --git a/Client/src/api/posts.ts b/Client/src/api/posts.ts new file mode 100644 index 0000000..3d168b5 --- /dev/null +++ b/Client/src/api/posts.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 +}