feat(client): photos API client + expose auth token
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
07b0299438
commit
9685336903
@ -61,7 +61,7 @@ function App() {
|
|||||||
if (auth.username) {
|
if (auth.username) {
|
||||||
// 已登录:照片墙入口 → 照片墙,否则博客页。
|
// 已登录:照片墙入口 → 照片墙,否则博客页。
|
||||||
view = wantsPhoto
|
view = wantsPhoto
|
||||||
? <PhotoWallPage onExit={goStart} />
|
? <PhotoWallPage onExit={goStart} token={auth.token ?? ''} />
|
||||||
: <BlogPage username={auth.username} onSignOut={goStart} />
|
: <BlogPage username={auth.username} onSignOut={goStart} />
|
||||||
} else if (wantsPhoto) {
|
} else if (wantsPhoto) {
|
||||||
// #photo 未登录:复用登录页,但去掉顶部服务器负载栏。
|
// #photo 未登录:复用登录页,但去掉顶部服务器负载栏。
|
||||||
|
|||||||
57
Client/src/api/photos.ts
Normal file
57
Client/src/api/photos.ts
Normal 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())
|
||||||
|
}
|
||||||
@ -21,6 +21,8 @@ import {
|
|||||||
interface PhotoWallPageProps {
|
interface PhotoWallPageProps {
|
||||||
/** 退出照片墙(清登录态 + 抹掉地址栏 #photo,回到导航页) */
|
/** 退出照片墙(清登录态 + 抹掉地址栏 #photo,回到导航页) */
|
||||||
onExit: () => void
|
onExit: () => void
|
||||||
|
/** 当前会话 JWT,供后续服务端图片接口使用(Task 6 正式消费) */
|
||||||
|
token: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ElType = 'photo' | 'note' | 'tape' | 'sticker' | 'stamp' | 'doodle'
|
type ElType = 'photo' | 'note' | 'tape' | 'sticker' | 'stamp' | 'doodle'
|
||||||
@ -115,7 +117,8 @@ function loadInitial() {
|
|||||||
return { els, bgIndex, nextId, maxZ }
|
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 initial = useMemo(() => loadInitial(), [])
|
||||||
|
|
||||||
const [els, setEls] = useState<El[]>(initial.els)
|
const [els, setEls] = useState<El[]>(initial.els)
|
||||||
|
|||||||
@ -35,6 +35,8 @@ function loadSession(): Session | null {
|
|||||||
export interface Auth {
|
export interface Auth {
|
||||||
/** 已登录则为当前用户名,否则为 null */
|
/** 已登录则为当前用户名,否则为 null */
|
||||||
username: string | null
|
username: string | null
|
||||||
|
/** 当前会话 token(JWT),未登录为 null。用于受保护接口的 Authorization 头 */
|
||||||
|
token: string | null
|
||||||
/** 校验凭据;remember 为 true 时持久化到 localStorage,否则只存 sessionStorage */
|
/** 校验凭据;remember 为 true 时持久化到 localStorage,否则只存 sessionStorage */
|
||||||
signIn: (username: string, password: string, remember: boolean) => Promise<void>
|
signIn: (username: string, password: string, remember: boolean) => Promise<void>
|
||||||
/** 退出登录并清除浏览器里的会话(两种 storage 都清) */
|
/** 退出登录并清除浏览器里的会话(两种 storage 都清) */
|
||||||
@ -73,5 +75,5 @@ export function useAuth(): Auth {
|
|||||||
sessionStorage.removeItem(STORAGE_KEY)
|
sessionStorage.removeItem(STORAGE_KEY)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return { username: session?.username ?? null, signIn, signOut }
|
return { username: session?.username ?? null, token: session?.token ?? null, signIn, signOut }
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user