@ -10,20 +10,30 @@
|
||||
// 即便网络失败也不回滚本地,保证前端不"卡死"。
|
||||
// - 未登录:纯本地 localStorage,行为与旧版一致(按当前用户名隔离)。
|
||||
//
|
||||
// 对外函数:getFavorites / isFavorited / toggleFavorite / removeFavorite /
|
||||
// 两种收藏粒度(共用一张 favorites 表,后端以 kind 区分):
|
||||
// - gallery:整篇图集。id = 图集 uid(r/s 前缀),type = runway / street。
|
||||
// - image :单张图片。id = 图片自身的编码 id(i/j 前缀),type = runway_image / snap_image,
|
||||
// 额外带 imageUrl(这一张图)与 parentUid(所属图集,供从收藏回跳原图集)。
|
||||
// 两种粒度的 id 前缀互不冲突,因此可放在同一张表、同一套接口里。
|
||||
//
|
||||
// 对外函数:getFavorites / isFavorited / toggleFavorite / toggleImageFavorite / removeFavorite /
|
||||
// fetchFavoritesPage / checkFavorited(列表与个人中心按需调用)。
|
||||
|
||||
import { getUser, getAccessToken, authJson } from "./api"
|
||||
|
||||
export type FavoriteType = "runway" | "street"
|
||||
export type FavoriteType = "runway" | "street" | "runway_image" | "snap_image"
|
||||
export type FavoriteKind = "gallery" | "image"
|
||||
|
||||
export interface Favorite {
|
||||
id: string // 走秀/街拍的对外编码 id(带 r=/s= 前缀),与后端 target_uid 对齐
|
||||
id: string // 图集 uid(r/s 前缀)或图片编码 id(i/j 前缀),与后端 target_uid 对齐
|
||||
type: FavoriteType
|
||||
title: string
|
||||
cover: string
|
||||
brand: string
|
||||
addedAt: number
|
||||
kind?: FavoriteKind // 缺省视为 gallery(兼容升级前的本地数据)
|
||||
imageUrl?: string // kind=image:这一张图
|
||||
parentUid?: string // kind=image:所属图集 uid
|
||||
}
|
||||
|
||||
// 后端 FavoriteItem 响应结构({data:{items:[...]}} 解包后)
|
||||
@ -34,6 +44,9 @@ interface ServerFavorite {
|
||||
cover?: string
|
||||
brand?: string
|
||||
added_at?: number
|
||||
kind?: string
|
||||
image_url?: string
|
||||
parent_uid?: string
|
||||
}
|
||||
|
||||
// ---------- 本地存储(兜底 / 离线) ----------
|
||||
@ -66,6 +79,15 @@ function serverMode(): boolean {
|
||||
return !!(getUser() && getAccessToken())
|
||||
}
|
||||
|
||||
// ---------- 归一化(后端字符串 → 联合类型) ----------
|
||||
function normalizeType(t?: string): FavoriteType {
|
||||
if (t === "street" || t === "runway_image" || t === "snap_image") return t
|
||||
return "runway"
|
||||
}
|
||||
function normalizeKind(k?: string): FavoriteKind {
|
||||
return k === "image" ? "image" : "gallery"
|
||||
}
|
||||
|
||||
// ---------- 对外 API ----------
|
||||
|
||||
// 本地收藏(兜底 / 离线态)。列表卡片打勾时用它做"即时填充"。
|
||||
@ -98,6 +120,31 @@ export function toggleFavorite(item: Omit<Favorite, "addedAt">): boolean {
|
||||
return newState
|
||||
}
|
||||
|
||||
/**
|
||||
* 收藏 / 取消单张图片(比 toggleFavorite 更省事的语义化封装)。
|
||||
* cover 直接设为该图 URL,个人中心网格即可原样渲染这张图;
|
||||
* parentUid 记下所属图集,收藏卡片点击时回跳到原图集。
|
||||
*/
|
||||
export function toggleImageFavorite(args: {
|
||||
imageId: string
|
||||
type: "runway_image" | "snap_image"
|
||||
imageUrl: string
|
||||
parentUid: string
|
||||
title?: string
|
||||
brand?: string
|
||||
}): boolean {
|
||||
return toggleFavorite({
|
||||
id: args.imageId,
|
||||
type: args.type,
|
||||
title: args.title || "",
|
||||
cover: args.imageUrl,
|
||||
brand: args.brand || "",
|
||||
kind: "image",
|
||||
imageUrl: args.imageUrl,
|
||||
parentUid: args.parentUid,
|
||||
})
|
||||
}
|
||||
|
||||
export function removeFavorite(id: string): void {
|
||||
setLocal(getLocal().filter((f) => f.id !== id))
|
||||
if (serverMode()) {
|
||||
@ -116,26 +163,36 @@ export interface FavPageResult {
|
||||
|
||||
/**
|
||||
* 拉取收藏的某一页(默认 24 条/页)。
|
||||
* kind 传 gallery / image 可只取对应粒度(服务端过滤),留空为全部。
|
||||
* 返回 { items, total, lastPage },前端据此渲染当前页 + 翻页器。
|
||||
* 未登录回落本地收藏(无分页)。
|
||||
*/
|
||||
export async function fetchFavoritesPage(page = 1, perPage = 24): Promise<FavPageResult> {
|
||||
export async function fetchFavoritesPage(
|
||||
page = 1,
|
||||
perPage = 24,
|
||||
kind?: FavoriteKind,
|
||||
): Promise<FavPageResult> {
|
||||
if (!serverMode()) {
|
||||
const local = getLocal()
|
||||
return { items: local, total: local.length, lastPage: 1 }
|
||||
const items = kind ? local.filter((f) => normalizeKind(f.kind) === kind) : local
|
||||
return { items, total: items.length, lastPage: 1 }
|
||||
}
|
||||
try {
|
||||
const qs = kind ? `&kind=${kind}` : ""
|
||||
const r = await authJson<{ items: ServerFavorite[]; total: number; last_page: number }>(
|
||||
"GET",
|
||||
`/auth/favorites?page=${page}&per_page=${perPage}`,
|
||||
`/auth/favorites?page=${page}&per_page=${perPage}${qs}`,
|
||||
)
|
||||
const items = (r?.items ?? []).map((it) => ({
|
||||
id: String(it.id),
|
||||
type: it.type === "street" ? "street" : "runway",
|
||||
type: normalizeType(it.type),
|
||||
title: it.title || "",
|
||||
cover: it.cover || "",
|
||||
brand: it.brand || "",
|
||||
addedAt: Number(it.added_at) || 0,
|
||||
kind: normalizeKind(it.kind),
|
||||
imageUrl: it.image_url || "",
|
||||
parentUid: it.parent_uid || "",
|
||||
}))
|
||||
return {
|
||||
items,
|
||||
@ -144,14 +201,16 @@ export async function fetchFavoritesPage(page = 1, perPage = 24): Promise<FavPag
|
||||
}
|
||||
} catch {
|
||||
const local = getLocal()
|
||||
return { items: local, total: local.length, lastPage: 1 }
|
||||
const items = kind ? local.filter((f) => normalizeKind(f.kind) === kind) : local
|
||||
return { items, total: items.length, lastPage: 1 }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 列表卡片:批量校验当前页收藏态 ----------
|
||||
// ---------- 列表卡片 / 详情页:批量校验当前页收藏态 ----------
|
||||
/**
|
||||
* 传入当前可见的卡片 id 列表,返回「已收藏」的 id 集合。
|
||||
* 后端只对这些 id 做 WHERE target_uid IN (...) 查询,复杂度与收藏总量无关。
|
||||
* 图集 id 与图片 id 可混传(前缀不同,互不影响)。
|
||||
*/
|
||||
export async function checkFavorited(ids: string[]): Promise<Set<string>> {
|
||||
if (!serverMode() || ids.length === 0) return new Set()
|
||||
@ -164,10 +223,27 @@ export async function checkFavorited(ids: string[]): Promise<Set<string>> {
|
||||
}
|
||||
|
||||
/** 服务端校验命中时把该篇补进本地收藏(详情页跨设备校正用;不触发 POST——服务端已有该收藏)。 */
|
||||
export function hydrateFavorited(id: string, type: FavoriteType, title = "", cover = "", brand = ""): void {
|
||||
export function hydrateFavorited(
|
||||
id: string,
|
||||
type: FavoriteType,
|
||||
title = "",
|
||||
cover = "",
|
||||
brand = "",
|
||||
extra?: { kind?: FavoriteKind; imageUrl?: string; parentUid?: string },
|
||||
): void {
|
||||
const list = getLocal()
|
||||
if (list.some((f) => f.id === id)) return
|
||||
list.unshift({ id, type, title, cover, brand, addedAt: Date.now() })
|
||||
list.unshift({
|
||||
id,
|
||||
type,
|
||||
title,
|
||||
cover,
|
||||
brand,
|
||||
addedAt: Date.now(),
|
||||
kind: normalizeKind(extra?.kind),
|
||||
imageUrl: extra?.imageUrl || "",
|
||||
parentUid: extra?.parentUid || "",
|
||||
})
|
||||
setLocal(list)
|
||||
}
|
||||
|
||||
@ -179,6 +255,9 @@ function syncServer(item: Omit<Favorite, "addedAt">, newState: boolean): void {
|
||||
title: item.title,
|
||||
cover: item.cover,
|
||||
brand: item.brand,
|
||||
kind: normalizeKind(item.kind),
|
||||
image_url: item.imageUrl || "",
|
||||
parent_uid: item.parentUid || "",
|
||||
}
|
||||
if (newState) {
|
||||
authJson("POST", "/auth/favorites", payload).catch(() => {
|
||||
|
||||
Reference in New Issue
Block a user