feat(db): 内容库迁入 SQLite,分类与侧栏导航改为数据库驱动
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { fetchCatalog, subscribeCatalog } from './api'
|
||||
import { EMPTY_NAV, type NavModel } from './data/nav'
|
||||
import type { Category } from './data/types'
|
||||
|
||||
export type CatalogStatus = 'loading' | 'ready' | 'error'
|
||||
|
||||
export type CatalogState = {
|
||||
status: CatalogStatus
|
||||
categories: Category[]
|
||||
/** 侧栏导航;与分类同一次请求下发 */
|
||||
nav: NavModel
|
||||
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: [],
|
||||
nav: EMPTY_NAV,
|
||||
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,
|
||||
nav: catalog.nav,
|
||||
error: null,
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (!controller.signal.aborted) {
|
||||
setState((previous) => ({
|
||||
// 已经有内容时只提示错误,不清空列表
|
||||
status: previous.categories.length > 0 ? 'ready' : 'error',
|
||||
categories: previous.categories,
|
||||
nav: previous.nav,
|
||||
error: toErrorMessage(error),
|
||||
}))
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
// 首屏加载:setState 只发生在 Promise 回调里,避免 effect 内同步 setState
|
||||
useEffect(() => {
|
||||
load()
|
||||
return () => controllerRef.current?.abort()
|
||||
}, [load])
|
||||
|
||||
// 内容库被外部修改时,服务端推送一次「变了」,这里重新拉取
|
||||
useEffect(() => subscribeCatalog(load), [load])
|
||||
|
||||
return { ...state, reload: load }
|
||||
}
|
||||
Reference in New Issue
Block a user