This commit is contained in:
toom1996
2026-09-13 00:49:42 +08:00
parent 19fb597bb4
commit c85c561a99
19 changed files with 552 additions and 245 deletions

View File

@ -20,9 +20,19 @@ export interface HistoryData {
}
export interface HistoryEntry {
id: string // 文章对外编码串:r=xxx(走秀) / s=xxx(街拍)
id: string // 对外编码串(服务端存储的原样值):r=xxx(走秀) / s=xxx(街拍)。删除时必须原样回传
viewedAt: number
data?: HistoryData // 账户页展示时回查详情获得;本地/回查失败时缺失(卡片占位渲染)
canonId?: string // 回查得到的「规范 id」(带类型前缀)。早期数据可能缺前缀(如 0008CZYI),
// 同一篇文章因此会存成两行;链接统一用规范 id,保证只指向一个地址。
}
// 去掉一个「类型前缀」后的串,作为「同一篇文章」的判据:r=走秀 / s=街拍 / i=走秀单图 / j=街拍单图。
// 早期历史数据没有类型前缀(0008CZYI),与新数据(r0008CZYI)指向同一篇,只按原样 id 去重会漏,
// 故本地缓存按「去前缀」后的串判同。只剥已知前缀,避免误伤本身以字母开头的 hashid 本体。
function dedupeKey(id: string): string {
const s = String(id || "")
return /^[rsij]/.test(s) ? s.slice(1) : s
}
// 后端 HistoryItem 响应结构({data:{items:[...]}} 解包后,仅 id + viewed_at)
@ -71,8 +81,9 @@ function serverMode(): boolean {
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)
// 本地去重:按「去类型前缀」后的串判同,避免同一篇文章因 id 形式不同(旧数据无前缀)留两条并排
const key = dedupeKey(id)
const list = getLocal().filter((h) => dedupeKey(h.id) !== key)
list.unshift(entry)
if (list.length > 200) list.length = 200
setLocal(list)
@ -90,12 +101,26 @@ export interface HistPageResult {
lastPage: number
}
// 按「规范 id」收敛同一篇文章,保留首次出现的那条(服务端已按 viewed_at 倒序 → 即最新的一条)。
// 用于消除「同一篇文章两行并排」:早期历史数据没有类型前缀,与带前缀的新数据指向同一篇。
function dedupeEntries(list: HistoryEntry[]): HistoryEntry[] {
const seen = new Set<string>()
const out: HistoryEntry[] = []
for (const h of list) {
const k = h.canonId || dedupeKey(h.id)
if (seen.has(k)) continue
seen.add(k)
out.push(h)
}
return out
}
/**
* 拉取浏览历史的一页(登录态走服务端分页 + 逐篇回查公开详情;未登录回落本地缓存)。
*/
export async function fetchHistoryPage(page = 1, perPage = 24): Promise<HistPageResult> {
if (!serverMode()) {
const local = getLocal()
const local = dedupeEntries(getLocal())
return { items: local, total: local.length, lastPage: 1 }
}
try {
@ -113,23 +138,29 @@ export async function fetchHistoryPage(page = 1, perPage = 24): Promise<HistPage
base.map(async (h) => {
try {
const meta = await getHistoryMeta(h.id)
if (meta) h.data = meta
if (meta) {
h.data = meta
h.canonId = meta.id // 回查同时拿到规范 id(无前缀的旧 id 也能拿到带前缀的结果)
}
} catch {
/* 单条回查失败:保留无 data,卡片走占位渲染 */
}
return h
}),
)
return { items, total: Number(r?.total || 0), lastPage: Number(r?.last_page || 1) }
// 收敛同一篇文章的历史行(旧数据缺前缀会与正常行并存);删除仍用原样 id,故不影响删除。
return { items: dedupeEntries(items), total: Number(r?.total || 0), lastPage: Number(r?.last_page || 1) }
} catch {
const local = getLocal()
const local = dedupeEntries(getLocal())
return { items: local, total: local.length, lastPage: 1 }
}
}
/** 删除一条历史(按文章 id)。 */
export function removeHistory(id: string): void {
setLocal(getLocal().filter((h) => h.id !== id))
// 本地按「去类型前缀」判同删除,避免旧数据留下同篇的孪生行
const key = dedupeKey(id)
setLocal(getLocal().filter((h) => dedupeKey(h.id) !== key))
if (serverMode()) {
authJson("DELETE", `/me/history/${encodeURIComponent(id)}`).catch(() => {
/* 失败不回滚本地 */