- 新增 data/view.ts 与 useViewRoute:视图与地址双向同步,支持前进后退 - 条目地址带上所属分组,刷新 / 收藏后「返回」有确定落点 - 服务端只把 /c/** 回落到 index.html,其余路径仍返回 404 - 明细页返回按钮去掉左侧箭头图标
80 lines
2.4 KiB
TypeScript
80 lines
2.4 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
import { fetchCatalog, subscribeCatalog } from './api'
|
|
import type { Category } from './data/types'
|
|
|
|
export type CatalogStatus = 'loading' | 'ready' | 'error'
|
|
|
|
export type CatalogState = {
|
|
status: CatalogStatus
|
|
categories: Category[]
|
|
error: string | null
|
|
}
|
|
|
|
export type CatalogController = CatalogState & {
|
|
/** 手动重新拉取;内容库被外部修改后也会自动调用 */
|
|
reload: () => void
|
|
}
|
|
|
|
function toErrorMessage(error: unknown): string {
|
|
return error instanceof Error ? error.message : String(error)
|
|
}
|
|
|
|
/**
|
|
* 内容目录:首屏拉取 + 监听服务端推送 + 手动重载。
|
|
*
|
|
* 与 `useProjects` 的区别是失败策略:项目列表失败会保留旧数据,
|
|
* 内容目录同样保留旧数据 —— 用户正在改库时偶尔读到坏 JSON 不应该把页面清空。
|
|
*/
|
|
export function useCatalog(): CatalogController {
|
|
const [state, setState] = useState<CatalogState>({
|
|
status: 'loading',
|
|
categories: [],
|
|
error: null,
|
|
})
|
|
const controllerRef = useRef<AbortController | null>(null)
|
|
|
|
/**
|
|
* 拉一次内容目录。
|
|
*
|
|
* 用 ref 保存当前的 AbortController:外部改动可能连续触发多次刷新,
|
|
* 只保留最后一次请求,避免旧响应把新数据覆盖回去。
|
|
*/
|
|
const load = useCallback(() => {
|
|
controllerRef.current?.abort()
|
|
const controller = new AbortController()
|
|
controllerRef.current = controller
|
|
|
|
fetchCatalog(controller.signal)
|
|
.then((catalog) => {
|
|
if (!controller.signal.aborted) {
|
|
setState({
|
|
status: 'ready',
|
|
categories: catalog.categories,
|
|
error: null,
|
|
})
|
|
}
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (!controller.signal.aborted) {
|
|
setState((previous) => ({
|
|
// 已经有内容时只提示错误,不清空列表
|
|
status: previous.categories.length > 0 ? 'ready' : 'error',
|
|
categories: previous.categories,
|
|
error: toErrorMessage(error),
|
|
}))
|
|
}
|
|
})
|
|
}, [])
|
|
|
|
// 首屏加载:setState 只发生在 Promise 回调里,避免 effect 内同步 setState
|
|
useEffect(() => {
|
|
load()
|
|
return () => controllerRef.current?.abort()
|
|
}, [load])
|
|
|
|
// 内容库被外部修改时,服务端推送一次「变了」,这里重新拉取
|
|
useEffect(() => subscribeCatalog(load), [load])
|
|
|
|
return { ...state, reload: load }
|
|
}
|