148
src/lib/history.ts
Normal file
148
src/lib/history.ts
Normal file
@ -0,0 +1,148 @@
|
||||
// src/lib/history.ts — 浏览历史(服务端持久化 + 本地缓存兜底)
|
||||
//
|
||||
// 2026-09-02 语义收敛(用户拍板):
|
||||
// - 浏览历史 = 「点开过哪篇文章」,触发时机在 item 详情页(打开某篇走秀/街拍即记一条)。
|
||||
// 列表页(Runway Looks / Street Snaps)进入不再记。
|
||||
// - 后端 histories 表只存 (user_id, target_uid, viewed_at):target_uid 为文章对外编码串
|
||||
// (r= 前缀走秀 / s= 前缀街拍,编码串自带类型,故不再有 kind/target_type 概念),
|
||||
// 表已去掉 title/cover/brand 快照列。
|
||||
// - 账户页 My History 展示:先取服务端 id 列表,再逐篇回查公开详情接口拿 title/cover/brand
|
||||
// (用户选「只存 id、展示时回查」),历史卡片点击跳回文章详情。
|
||||
// - 未登录:纯本地 localStorage(按用户名隔离);登录态写本地 + 后台 POST(失败不回滚本地)。
|
||||
|
||||
import { getUser, getAccessToken, authJson, getHistoryMeta } from "./api"
|
||||
|
||||
export interface HistoryData {
|
||||
title: string
|
||||
cover: string
|
||||
brand: string
|
||||
type: "street" | "runway"
|
||||
}
|
||||
|
||||
export interface HistoryEntry {
|
||||
id: string // 文章对外编码串:r=xxx(走秀) / s=xxx(街拍)
|
||||
viewedAt: number
|
||||
data?: HistoryData // 账户页展示时回查详情获得;本地/回查失败时缺失(卡片占位渲染)
|
||||
}
|
||||
|
||||
// 后端 HistoryItem 响应结构({data:{items:[...]}} 解包后,仅 id + viewed_at)
|
||||
interface ServerHistory {
|
||||
id: string
|
||||
viewed_at?: number
|
||||
}
|
||||
|
||||
// ---------- 本地存储(兜底 / 离线) ----------
|
||||
function storageKey(): string | null {
|
||||
const u = getUser()
|
||||
return u ? `fa_hist_${u.username}` : null
|
||||
}
|
||||
function getLocal(): HistoryEntry[] {
|
||||
const k = storageKey()
|
||||
if (!k) return []
|
||||
try {
|
||||
const arr = JSON.parse(localStorage.getItem(k) || "[]")
|
||||
return Array.isArray(arr) ? arr : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
function setLocal(list: HistoryEntry[]): void {
|
||||
const k = storageKey()
|
||||
if (!k) return
|
||||
try {
|
||||
localStorage.setItem(k, JSON.stringify(list))
|
||||
} catch {
|
||||
/* 隐私模式等写入失败,忽略 */
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 服务端模式判定 ----------
|
||||
function serverMode(): boolean {
|
||||
return !!(getUser() && getAccessToken())
|
||||
}
|
||||
|
||||
// ---------- 对外 API ----------
|
||||
|
||||
/**
|
||||
* 记录一次浏览:打开某篇文章详情时调用(见 item 详情页)。
|
||||
* 登录态:写入本地缓存 + 后台 POST 服务端(upsert 刷新时间,失败不回滚本地)。
|
||||
* 未登录:仅写入本地缓存。
|
||||
*/
|
||||
export function recordHistory(id: string): void {
|
||||
if (!id) return
|
||||
const entry: HistoryEntry = { id, viewedAt: Date.now() }
|
||||
// 本地去重(同 id 只保留一条并提到最前)
|
||||
const list = getLocal().filter((h) => h.id !== id)
|
||||
list.unshift(entry)
|
||||
if (list.length > 200) list.length = 200
|
||||
setLocal(list)
|
||||
|
||||
if (serverMode()) {
|
||||
authJson("POST", "/auth/history", { target_uid: id }).catch(() => {
|
||||
/* 失败不回滚本地 */
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export interface HistPageResult {
|
||||
items: HistoryEntry[]
|
||||
total: number
|
||||
lastPage: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取浏览历史的一页(登录态走服务端分页 + 逐篇回查公开详情;未登录回落本地缓存)。
|
||||
*/
|
||||
export async function fetchHistoryPage(page = 1, perPage = 24): Promise<HistPageResult> {
|
||||
if (!serverMode()) {
|
||||
const local = getLocal()
|
||||
return { items: local, total: local.length, lastPage: 1 }
|
||||
}
|
||||
try {
|
||||
const qs = `page=${page}&per_page=${perPage}`
|
||||
const r = await authJson<{ items: ServerHistory[]; total: number; last_page: number }>(
|
||||
"GET",
|
||||
`/auth/history?${qs}`,
|
||||
)
|
||||
const base: HistoryEntry[] = (r?.items ?? []).map((it) => ({
|
||||
id: String(it.id),
|
||||
viewedAt: Number(it.viewed_at) || 0,
|
||||
}))
|
||||
// 逐篇回查公开详情接口拿展示数据(每篇 1 次;单条失败容忍,卡片仍可点击)
|
||||
const items = await Promise.all(
|
||||
base.map(async (h) => {
|
||||
try {
|
||||
const meta = await getHistoryMeta(h.id)
|
||||
if (meta) h.data = meta
|
||||
} catch {
|
||||
/* 单条回查失败:保留无 data,卡片走占位渲染 */
|
||||
}
|
||||
return h
|
||||
}),
|
||||
)
|
||||
return { items, total: Number(r?.total || 0), lastPage: Number(r?.last_page || 1) }
|
||||
} catch {
|
||||
const local = getLocal()
|
||||
return { items: local, total: local.length, lastPage: 1 }
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除一条历史(按文章 id)。 */
|
||||
export function removeHistory(id: string): void {
|
||||
setLocal(getLocal().filter((h) => h.id !== id))
|
||||
if (serverMode()) {
|
||||
authJson("DELETE", `/auth/history/${encodeURIComponent(id)}`).catch(() => {
|
||||
/* 失败不回滚本地 */
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 清空全部历史。 */
|
||||
export function clearHistory(): void {
|
||||
setLocal([])
|
||||
if (serverMode()) {
|
||||
authJson("DELETE", "/auth/history").catch(() => {
|
||||
/* 失败不回滚本地 */
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user