mdStore.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. // mdStore.ts
  2. import { create } from "zustand";
  3. import axios from "axios";
  4. import { updateMarkdownContent, UpdateMarkdownRequest } from "@/api/extract"; // 确保路径正确
  5. interface MdContent {
  6. content: string;
  7. isLoading: boolean;
  8. }
  9. type AnchorType =
  10. | "span"
  11. | "div"
  12. | "comment"
  13. | "data-attribute"
  14. | "hr"
  15. | "mark"
  16. | "p";
  17. interface AnchorOptions {
  18. type: AnchorType;
  19. prefix?: string;
  20. style?: string;
  21. className?: string;
  22. customAttributes?: Record<string, string>;
  23. }
  24. const defaultAnchorOptions: AnchorOptions = {
  25. type: "span",
  26. prefix: "md-anchor-",
  27. style: "display:none;",
  28. className: "",
  29. customAttributes: {},
  30. };
  31. interface MdState {
  32. mdContents: Record<string, MdContent>;
  33. allMdContent: string;
  34. allMdContentWithAnchor: string;
  35. error: Error | null;
  36. currentRequestId: number;
  37. setMdUrlArr: (urls: string[]) => Promise<void>;
  38. getAllMdContent: (data: string[]) => string;
  39. setAllMdContent: (val?: string) => void;
  40. setAllMdContentWithAnchor: (val?: string) => void;
  41. getContentWithAnchors: (
  42. data: string[],
  43. options?: Partial<AnchorOptions>
  44. ) => string;
  45. jumpToAnchor: (anchorId: string) => number;
  46. reset: () => void;
  47. updateMdContent: (
  48. fileKey: string,
  49. pageNumber: string | number,
  50. newContent: string
  51. ) => Promise<void>;
  52. }
  53. const MAX_CONCURRENT_REQUESTS = 2;
  54. const initialState = {
  55. mdContents: {},
  56. allMdContent: "",
  57. allMdContentWithAnchor: "",
  58. error: null,
  59. currentRequestId: 0,
  60. };
  61. const useMdStore = create<MdState>((set, get) => ({
  62. ...initialState,
  63. reset: () => {
  64. set(initialState);
  65. },
  66. setAllMdContent: (value?: string) => {
  67. set(() => ({
  68. allMdContent: value,
  69. }));
  70. },
  71. setAllMdContentWithAnchor: (value?: string) => {
  72. set(() => ({
  73. allMdContentWithAnchor: value,
  74. }));
  75. },
  76. setMdUrlArr: async (urls: string[]) => {
  77. const requestId = get().currentRequestId + 1;
  78. set((state) => ({ currentRequestId: requestId, error: null }));
  79. const fetchContent = async (url: string): Promise<[string, string]> => {
  80. try {
  81. const response = await axios.get<string>(url);
  82. return [url, response.data];
  83. } catch (error) {
  84. if (get().currentRequestId === requestId) {
  85. set((state) => ({ error: error as Error }));
  86. }
  87. return [url, ""];
  88. }
  89. };
  90. const fetchWithConcurrency = async (
  91. urls: string[]
  92. ): Promise<[string, string][]> => {
  93. const queue = [...urls];
  94. const results: [string, string][] = [];
  95. const inProgress = new Set<Promise<[string, string]>>();
  96. while (queue.length > 0 || inProgress.size > 0) {
  97. while (inProgress.size < MAX_CONCURRENT_REQUESTS && queue.length > 0) {
  98. const url = queue.shift()!;
  99. const promise = fetchContent(url);
  100. inProgress.add(promise);
  101. promise.then((result) => {
  102. results.push(result);
  103. inProgress.delete(promise);
  104. });
  105. }
  106. if (inProgress.size > 0) {
  107. await Promise.race(inProgress);
  108. }
  109. }
  110. return results;
  111. };
  112. const results = await fetchWithConcurrency(urls);
  113. if (get().currentRequestId === requestId) {
  114. const newMdContents: Record<string, MdContent> = {};
  115. results.forEach(([url, content]) => {
  116. newMdContents[url] = { content, isLoading: false };
  117. });
  118. set((state) => ({
  119. mdContents: newMdContents,
  120. allMdContent: state.getAllMdContent(results.map((i) => i[1])),
  121. allMdContentWithAnchor: state.getContentWithAnchors(
  122. results.map((i) => i[1])
  123. ),
  124. }));
  125. }
  126. },
  127. getAllMdContent: (data) => {
  128. return data?.join("\n\n");
  129. },
  130. getContentWithAnchors: (data: string[], options?: Partial<AnchorOptions>) => {
  131. const opts = { ...defaultAnchorOptions, ...options };
  132. const generateAnchorTag = (index: number) => {
  133. const id = `${opts.prefix}${index}`;
  134. const attributes = Object.entries(opts.customAttributes || {})
  135. .map(([key, value]) => `${key}="${value}"`)
  136. .join(" ");
  137. switch (opts.type) {
  138. case "span":
  139. case "div":
  140. case "mark":
  141. case "p":
  142. return `<${opts.type} id="${id}" style="${opts.style}" class="${opts.className}" ${attributes}></${opts.type}>`;
  143. case "comment":
  144. return `<!-- anchor: ${id} -->`;
  145. case "data-attribute":
  146. return `<span data-anchor="${id}" style="${opts.style}" class="${opts.className}" ${attributes}></span>`;
  147. case "hr":
  148. return `<hr id="${id}" style="${opts.style}" class="${opts.className}" ${attributes}>`;
  149. default:
  150. return `<span id="${id}" style="${opts.style}" class="${opts.className}" ${attributes}></span>`;
  151. }
  152. };
  153. return data
  154. ?.map((content, index) => {
  155. const anchorTag = generateAnchorTag(index);
  156. return `${anchorTag}\n\n${content}`;
  157. })
  158. .join("\n\n");
  159. },
  160. jumpToAnchor: (anchorId: string) => {
  161. const { mdContents } = get();
  162. const contentArray = Object.values(mdContents).map(
  163. (content) => content.content
  164. );
  165. let totalLength = 0;
  166. for (let i = 0; i < contentArray.length; i++) {
  167. if (anchorId === `md-anchor-${i}`) {
  168. return totalLength;
  169. }
  170. totalLength += contentArray[i].length + 2; // +2 for "\n\n"
  171. }
  172. return -1; // Anchor not found
  173. },
  174. updateMdContent: async (
  175. fileKey: string,
  176. pageNumber: string,
  177. newContent: string
  178. ) => {
  179. try {
  180. const params: UpdateMarkdownRequest = {
  181. file_key: fileKey,
  182. data: {
  183. [pageNumber]: newContent,
  184. },
  185. };
  186. const result = await updateMarkdownContent(params);
  187. if (result && result.success) {
  188. // 更新本地状态
  189. set((state) => {
  190. const updatedMdContents = { ...state.mdContents };
  191. if (updatedMdContents[fileKey]) {
  192. updatedMdContents[fileKey] = {
  193. ...updatedMdContents[fileKey],
  194. content: newContent,
  195. };
  196. }
  197. // 重新计算 allMdContent 和 allMdContentWithAnchor
  198. const contentArray = Object.values(updatedMdContents).map(
  199. (content) => content.content
  200. );
  201. const newAllMdContent = state.getAllMdContent(contentArray);
  202. const newAllMdContentWithAnchor =
  203. state.getContentWithAnchors(contentArray);
  204. return {
  205. mdContents: updatedMdContents,
  206. allMdContent: newAllMdContent,
  207. allMdContentWithAnchor: newAllMdContentWithAnchor,
  208. };
  209. });
  210. } else {
  211. throw new Error("Failed to update Markdown content");
  212. }
  213. } catch (error) {
  214. set({ error: error as Error });
  215. throw error;
  216. }
  217. },
  218. }));
  219. export default useMdStore;