From 9685336903d412e53dca7f92e30403eb76cbb0ac Mon Sep 17 00:00:00 2001 From: cc Date: Wed, 24 Jun 2026 21:18:43 +0800 Subject: [PATCH] feat(client): photos API client + expose auth token Co-Authored-By: Claude Opus 4.8 --- Client/src/App.tsx | 2 +- Client/src/api/photos.ts | 57 +++++++++++++++++++++++++ Client/src/components/PhotoWallPage.tsx | 5 ++- Client/src/hooks/useAuth.ts | 4 +- 4 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 Client/src/api/photos.ts diff --git a/Client/src/App.tsx b/Client/src/App.tsx index 15cc993..0e81055 100644 --- a/Client/src/App.tsx +++ b/Client/src/App.tsx @@ -61,7 +61,7 @@ function App() { if (auth.username) { // 已登录:照片墙入口 → 照片墙,否则博客页。 view = wantsPhoto - ? + ? : } else if (wantsPhoto) { // #photo 未登录:复用登录页,但去掉顶部服务器负载栏。 diff --git a/Client/src/api/photos.ts b/Client/src/api/photos.ts new file mode 100644 index 0000000..673030f --- /dev/null +++ b/Client/src/api/photos.ts @@ -0,0 +1,57 @@ +// 照片墙图片接口封装。基址与 auth.ts 一致:生产留空走同源相对路径。 +const API_BASE = + (import.meta.env.VITE_API_BASE as string | undefined) ?? 'http://localhost:8080' + +export interface PhotoItem { + id: string +} + +/** 上传失败的可区分原因,供 UI 给出对应提示 */ +export type UploadError = 'full' | 'too-large' | 'bad-type' | 'failed' + +function authHeaders(token: string): Record { + return { Authorization: `Bearer ${token}` } +} + +/** 列出当前用户的图片 id 与数量上限 */ +export async function listPhotos(token: string): Promise<{ items: PhotoItem[]; max: number }> { + const res = await fetch(`${API_BASE}/api/web/photos`, { headers: authHeaders(token) }) + if (!res.ok) throw new Error(`list failed: ${res.status}`) + return (await res.json()) as { items: PhotoItem[]; max: number } +} + +/** 上传一张图片(base64 data URL),返回服务器分配的图片 id */ +export async function uploadPhoto(token: string, dataUrl: string): Promise { + const res = await fetch(`${API_BASE}/api/web/photos`, { + method: 'POST', + headers: { ...authHeaders(token), 'Content-Type': 'application/json' }, + body: JSON.stringify({ dataUrl }), + }) + if (res.ok) return ((await res.json()) as { id: string }).id + const reason: UploadError = + res.status === 409 + ? 'full' + : res.status === 413 + ? 'too-large' + : res.status === 415 + ? 'bad-type' + : 'failed' + throw new Error(reason) +} + +/** 删除一张图片 */ +export async function deletePhoto(token: string, id: string): Promise { + const res = await fetch(`${API_BASE}/api/web/photos/${id}`, { + method: 'DELETE', + headers: authHeaders(token), + }) + if (!res.ok && res.status !== 404) throw new Error(`delete failed: ${res.status}`) +} + +/** 取图片字节为 objectURL(带鉴权,故不能直接用作 的服务器 URL)。 + * 调用方负责在不用时 URL.revokeObjectURL 释放。 */ +export async function fetchPhotoObjectUrl(token: string, id: string): Promise { + const res = await fetch(`${API_BASE}/api/web/photos/${id}`, { headers: authHeaders(token) }) + if (!res.ok) throw new Error(`get failed: ${res.status}`) + return URL.createObjectURL(await res.blob()) +} diff --git a/Client/src/components/PhotoWallPage.tsx b/Client/src/components/PhotoWallPage.tsx index 0075c47..db0c144 100644 --- a/Client/src/components/PhotoWallPage.tsx +++ b/Client/src/components/PhotoWallPage.tsx @@ -21,6 +21,8 @@ import { interface PhotoWallPageProps { /** 退出照片墙(清登录态 + 抹掉地址栏 #photo,回到导航页) */ onExit: () => void + /** 当前会话 JWT,供后续服务端图片接口使用(Task 6 正式消费) */ + token: string } type ElType = 'photo' | 'note' | 'tape' | 'sticker' | 'stamp' | 'doodle' @@ -115,7 +117,8 @@ function loadInitial() { return { els, bgIndex, nextId, maxZ } } -export function PhotoWallPage({ onExit }: PhotoWallPageProps) { +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function PhotoWallPage({ onExit, token: _token }: PhotoWallPageProps) { const initial = useMemo(() => loadInitial(), []) const [els, setEls] = useState(initial.els) diff --git a/Client/src/hooks/useAuth.ts b/Client/src/hooks/useAuth.ts index 1639093..875afd5 100644 --- a/Client/src/hooks/useAuth.ts +++ b/Client/src/hooks/useAuth.ts @@ -35,6 +35,8 @@ function loadSession(): Session | null { export interface Auth { /** 已登录则为当前用户名,否则为 null */ username: string | null + /** 当前会话 token(JWT),未登录为 null。用于受保护接口的 Authorization 头 */ + token: string | null /** 校验凭据;remember 为 true 时持久化到 localStorage,否则只存 sessionStorage */ signIn: (username: string, password: string, remember: boolean) => Promise /** 退出登录并清除浏览器里的会话(两种 storage 都清) */ @@ -73,5 +75,5 @@ export function useAuth(): Auth { sessionStorage.removeItem(STORAGE_KEY) }, []) - return { username: session?.username ?? null, signIn, signOut } + return { username: session?.username ?? null, token: session?.token ?? null, signIn, signOut } }