attachmentState.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. import { useCallback, useEffect, useRef, useState } from "react";
  2. import { HttpError } from "../../shared/api/errors";
  3. import type { PageResult } from "../../shared/api/types";
  4. import type { DocumentSummary, MainPlanBrief } from "../documents/documentTypes";
  5. import type { RemotePageState } from "../documents/documentState";
  6. import { attachmentApi } from "./attachmentApi";
  7. import type {
  8. AttachmentBindingSummary,
  9. AttachmentListQuery,
  10. MountedAttachmentQuery,
  11. } from "./attachmentTypes";
  12. import { useContentRevision } from "../../shared/state/contentRefresh";
  13. function toHttpError(error: unknown): HttpError {
  14. return error instanceof HttpError
  15. ? error
  16. : new HttpError({
  17. httpStatus: 0,
  18. code: "INTERNAL_ERROR",
  19. message: "请求失败",
  20. details: null,
  21. requestId: "unknown",
  22. cause: error,
  23. });
  24. }
  25. function usePage<T>(
  26. loader: (signal: AbortSignal) => Promise<PageResult<T>>,
  27. dependencies: readonly unknown[],
  28. debounceKey: string | undefined,
  29. enabled = true,
  30. debounceMs = 300,
  31. ): RemotePageState<T> {
  32. const [state, setState] = useState<RemotePageState<T>>({
  33. data: null,
  34. loading: true,
  35. error: null,
  36. retry: () => undefined,
  37. });
  38. const version = useRef(0);
  39. const previousDebounceKey = useRef(debounceKey);
  40. const [retryVersion, setRetryVersion] = useState(0);
  41. const retry = useCallback(() => setRetryVersion((value) => value + 1), []);
  42. useEffect(() => {
  43. if (!enabled) {
  44. setState((previous) => ({ ...previous, loading: false, error: null, retry }));
  45. return;
  46. }
  47. const current = ++version.current;
  48. const controller = new AbortController();
  49. const delay = previousDebounceKey.current === debounceKey ? 0 : debounceMs;
  50. previousDebounceKey.current = debounceKey;
  51. const timer = window.setTimeout(() => {
  52. setState((previous) => ({ ...previous, loading: true, error: null, retry }));
  53. void loader(controller.signal)
  54. .then((data) => {
  55. if (version.current === current) {
  56. setState({ data, loading: false, error: null, retry });
  57. }
  58. })
  59. .catch((error: unknown) => {
  60. if (version.current !== current || controller.signal.aborted) return;
  61. setState({ data: null, loading: false, error: toHttpError(error), retry });
  62. });
  63. }, delay);
  64. return () => {
  65. window.clearTimeout(timer);
  66. controller.abort();
  67. };
  68. // eslint-disable-next-line react-hooks/exhaustive-deps
  69. }, [...dependencies, enabled, retryVersion]);
  70. return { ...state, retry };
  71. }
  72. export function useAttachmentList(
  73. query: AttachmentListQuery,
  74. ): RemotePageState<DocumentSummary> {
  75. const revision = useContentRevision("attachments");
  76. return usePage(
  77. (signal) => attachmentApi.list(query, signal),
  78. [
  79. query.keyword,
  80. query.attachmentType,
  81. query.fileExtension,
  82. query.updatedFrom,
  83. query.updatedTo,
  84. query.sortBy,
  85. query.sortDirection,
  86. query.page,
  87. query.pageSize,
  88. revision,
  89. ],
  90. query.keyword,
  91. );
  92. }
  93. export function useMountedAttachments(
  94. mainPlanId: string | null,
  95. query: MountedAttachmentQuery,
  96. enabled = true,
  97. ): RemotePageState<AttachmentBindingSummary> {
  98. const documentRevision = useContentRevision("documents");
  99. const attachmentRevision = useContentRevision("attachments");
  100. return usePage(
  101. (signal) =>
  102. mainPlanId
  103. ? attachmentApi.mountedByMainPlan(mainPlanId, query, signal)
  104. : Promise.resolve({
  105. items: [],
  106. page: query.page ?? 1,
  107. pageSize: query.pageSize ?? 20,
  108. total: 0,
  109. totalPages: 0,
  110. }),
  111. [
  112. mainPlanId,
  113. query.keyword,
  114. query.attachmentType,
  115. query.updatedFrom,
  116. query.updatedTo,
  117. query.page,
  118. query.pageSize,
  119. documentRevision,
  120. attachmentRevision,
  121. ],
  122. query.keyword,
  123. enabled,
  124. );
  125. }
  126. export function useAttachmentMainPlans(id: string | null): {
  127. data: MainPlanBrief[] | null;
  128. loading: boolean;
  129. error: HttpError | null;
  130. } {
  131. const revision = useContentRevision("attachments");
  132. const [state, setState] = useState<{
  133. data: MainPlanBrief[] | null;
  134. loading: boolean;
  135. error: HttpError | null;
  136. }>({ data: null, loading: false, error: null });
  137. useEffect(() => {
  138. if (!id) {
  139. setState({ data: null, loading: false, error: null });
  140. return;
  141. }
  142. const controller = new AbortController();
  143. setState({ data: null, loading: true, error: null });
  144. void attachmentApi
  145. .mainPlans(id, controller.signal)
  146. .then((data) => setState({ data, loading: false, error: null }))
  147. .catch((error: unknown) => {
  148. if (!controller.signal.aborted) {
  149. setState({ data: null, loading: false, error: toHttpError(error) });
  150. }
  151. });
  152. return () => controller.abort();
  153. }, [id, revision]);
  154. return state;
  155. }