import { useCallback, useEffect, useRef, useState } from "react"; import { HttpError } from "../../shared/api/errors"; import type { PageResult } from "../../shared/api/types"; import type { DocumentSummary, MainPlanBrief } from "../documents/documentTypes"; import type { RemotePageState } from "../documents/documentState"; import { attachmentApi } from "./attachmentApi"; import type { AttachmentBindingSummary, AttachmentListQuery, MountedAttachmentQuery, } from "./attachmentTypes"; import { useContentRevision } from "../../shared/state/contentRefresh"; function toHttpError(error: unknown): HttpError { return error instanceof HttpError ? error : new HttpError({ httpStatus: 0, code: "INTERNAL_ERROR", message: "请求失败", details: null, requestId: "unknown", cause: error, }); } function usePage( loader: (signal: AbortSignal) => Promise>, dependencies: readonly unknown[], debounceKey: string | undefined, enabled = true, debounceMs = 300, ): RemotePageState { const [state, setState] = useState>({ data: null, loading: true, error: null, retry: () => undefined, }); const version = useRef(0); const previousDebounceKey = useRef(debounceKey); const [retryVersion, setRetryVersion] = useState(0); const retry = useCallback(() => setRetryVersion((value) => value + 1), []); useEffect(() => { if (!enabled) { setState((previous) => ({ ...previous, loading: false, error: null, retry })); return; } const current = ++version.current; const controller = new AbortController(); const delay = previousDebounceKey.current === debounceKey ? 0 : debounceMs; previousDebounceKey.current = debounceKey; const timer = window.setTimeout(() => { setState((previous) => ({ ...previous, loading: true, error: null, retry })); void loader(controller.signal) .then((data) => { if (version.current === current) { setState({ data, loading: false, error: null, retry }); } }) .catch((error: unknown) => { if (version.current !== current || controller.signal.aborted) return; setState({ data: null, loading: false, error: toHttpError(error), retry }); }); }, delay); return () => { window.clearTimeout(timer); controller.abort(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [...dependencies, enabled, retryVersion]); return { ...state, retry }; } export function useAttachmentList( query: AttachmentListQuery, ): RemotePageState { const revision = useContentRevision("attachments"); return usePage( (signal) => attachmentApi.list(query, signal), [ query.keyword, query.attachmentType, query.fileExtension, query.updatedFrom, query.updatedTo, query.sortBy, query.sortDirection, query.page, query.pageSize, revision, ], query.keyword, ); } export function useMountedAttachments( mainPlanId: string | null, query: MountedAttachmentQuery, enabled = true, ): RemotePageState { const documentRevision = useContentRevision("documents"); const attachmentRevision = useContentRevision("attachments"); return usePage( (signal) => mainPlanId ? attachmentApi.mountedByMainPlan(mainPlanId, query, signal) : Promise.resolve({ items: [], page: query.page ?? 1, pageSize: query.pageSize ?? 20, total: 0, totalPages: 0, }), [ mainPlanId, query.keyword, query.attachmentType, query.updatedFrom, query.updatedTo, query.page, query.pageSize, documentRevision, attachmentRevision, ], query.keyword, enabled, ); } export function useAttachmentMainPlans(id: string | null): { data: MainPlanBrief[] | null; loading: boolean; error: HttpError | null; } { const revision = useContentRevision("attachments"); const [state, setState] = useState<{ data: MainPlanBrief[] | null; loading: boolean; error: HttpError | null; }>({ data: null, loading: false, error: null }); useEffect(() => { if (!id) { setState({ data: null, loading: false, error: null }); return; } const controller = new AbortController(); setState({ data: null, loading: true, error: null }); void attachmentApi .mainPlans(id, controller.signal) .then((data) => setState({ data, loading: false, error: null })) .catch((error: unknown) => { if (!controller.signal.aborted) { setState({ data: null, loading: false, error: toHttpError(error) }); } }); return () => controller.abort(); }, [id, revision]); return state; }