feat(client): photos API client + expose auth token

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cc 2026-06-24 21:18:43 +08:00
parent 07b0299438
commit 9685336903
4 changed files with 65 additions and 3 deletions

View File

@ -61,7 +61,7 @@ function App() {
if (auth.username) {
// 已登录:照片墙入口 → 照片墙,否则博客页。
view = wantsPhoto
? <PhotoWallPage onExit={goStart} />
? <PhotoWallPage onExit={goStart} token={auth.token ?? ''} />
: <BlogPage username={auth.username} onSignOut={goStart} />
} else if (wantsPhoto) {
// #photo 未登录:复用登录页,但去掉顶部服务器负载栏。

57
Client/src/api/photos.ts Normal file
View File

@ -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<string, string> {
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<string> {
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<void> {
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 <img src> URL
* URL.revokeObjectURL */
export async function fetchPhotoObjectUrl(token: string, id: string): Promise<string> {
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())
}

View File

@ -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<El[]>(initial.els)

View File

@ -35,6 +35,8 @@ function loadSession(): Session | null {
export interface Auth {
/** 已登录则为当前用户名,否则为 null */
username: string | null
/** 当前会话 tokenJWT未登录为 null。用于受保护接口的 Authorization 头 */
token: string | null
/** 校验凭据remember 为 true 时持久化到 localStorage否则只存 sessionStorage */
signIn: (username: string, password: string, remember: boolean) => Promise<void>
/** 退出登录并清除浏览器里的会话(两种 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 }
}