2
0

ExportModal.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. import {
  2. Collapse,
  3. Modal,
  4. Progress,
  5. Select,
  6. Switch,
  7. Typography,
  8. message,
  9. } from "antd";
  10. import { useEffect, useRef, useState } from "react";
  11. import { get } from "../../request";
  12. import type { ArticleType } from "../article/Article"
  13. import ExportSettingLayout from "./ExportSettingLayout";
  14. const { Text } = Typography;
  15. interface IExportResponse {
  16. ok: boolean;
  17. message?: string;
  18. data: string;
  19. }
  20. interface IStatus {
  21. progress: number;
  22. message: string;
  23. log?: string[];
  24. }
  25. interface IExportStatusResponse {
  26. ok: boolean;
  27. message?: string;
  28. data: {
  29. url?: string;
  30. status: IStatus;
  31. };
  32. }
  33. interface IWidget {
  34. type?: ArticleType;
  35. articleId?: string;
  36. book?: string | null;
  37. para?: string | null;
  38. channelId?: string | null;
  39. anthologyId?: string | null;
  40. open?: boolean;
  41. onClose?: Function;
  42. }
  43. const ExportModalWidget = ({
  44. type,
  45. ___book,
  46. ___para,
  47. channelId,
  48. articleId,
  49. anthologyId,
  50. open = false,
  51. onClose,
  52. }: IWidget) => {
  53. const [isModalOpen, setIsModalOpen] = useState(open);
  54. const [filename, setFilename] = useState<string>();
  55. const [url, setUrl] = useState<string>();
  56. const [format, setFormat] = useState<string>("docx");
  57. const [exportStatus, setExportStatus] = useState<IStatus>();
  58. const [exportStart, setExportStart] = useState(false);
  59. const [hasOrigin, setHasOrigin] = useState(false);
  60. const [hasTranslation, setHasTranslation] = useState(true);
  61. const filenameRef = useRef(filename);
  62. useEffect(() => {
  63. // 及时更新 count 值
  64. filenameRef.current = filename;
  65. });
  66. const queryStatus = () => {
  67. if (typeof filenameRef.current === "undefined") {
  68. return;
  69. }
  70. const url = `/v2/export/${filenameRef.current}`;
  71. console.info("api request export", url);
  72. get<IExportStatusResponse>(url)
  73. .then((json) => {
  74. console.info("api response export", json);
  75. if (json.ok) {
  76. setExportStatus(json.data.status);
  77. if (json.data.status.progress === 1) {
  78. setFilename(undefined);
  79. setUrl(json.data.url);
  80. }
  81. } else {
  82. console.error(json.message);
  83. }
  84. })
  85. .catch((e) => console.error(e));
  86. };
  87. useEffect(() => {
  88. const interval = setInterval(() => queryStatus(), 3000);
  89. return () => clearInterval(interval);
  90. }, []);
  91. const getUrl = (): string => {
  92. if (!articleId) {
  93. throw new Error("id error");
  94. }
  95. let url = `/v2/export?type=${type}&id=${articleId}&format=${format}`;
  96. url += channelId ? `&channel=${channelId}` : "";
  97. url += "&origin=" + (hasOrigin ? "true" : "false");
  98. url += "&translation=" + (hasTranslation ? "true" : "false");
  99. switch (type) {
  100. case "chapter":
  101. const para = articleId?.split("-").map((item) => parseInt(item));
  102. if (para?.length === 2) {
  103. url += `&book=${para[0]}&par=${para[1]}`;
  104. } else {
  105. throw new Error("段落编号错误");
  106. }
  107. if (!channelId) {
  108. throw new Error("请选择版本");
  109. }
  110. break;
  111. case "article":
  112. url += `&id=${articleId}`;
  113. url += anthologyId ? `&anthology=${anthologyId}` : "";
  114. break;
  115. default:
  116. throw new Error("此类型暂时无法导出" + type);
  117. break;
  118. }
  119. return url;
  120. };
  121. const exportRun = (): void => {
  122. const url = getUrl();
  123. console.info("api request", url);
  124. setExportStart(true);
  125. get<IExportResponse>(url)
  126. .then((json) => {
  127. console.info("api response", json);
  128. if (json.ok) {
  129. const filename = json.data;
  130. console.log("filename", filename);
  131. setFilename(filename);
  132. } else {
  133. }
  134. })
  135. .catch((_e) => {});
  136. };
  137. const closeModal = () => {
  138. if (typeof onClose !== "undefined") {
  139. onClose();
  140. }
  141. };
  142. useEffect(() => setIsModalOpen(open), [open]);
  143. return (
  144. <Modal
  145. destroyOnClose
  146. title="导出"
  147. width={400}
  148. open={isModalOpen}
  149. onOk={() => {
  150. console.log("type", type);
  151. try {
  152. exportRun();
  153. } catch (error) {
  154. message.error((error as Error).message);
  155. console.error(error);
  156. }
  157. }}
  158. onCancel={closeModal}
  159. okText={"导出"}
  160. okButtonProps={{ disabled: exportStart }}
  161. >
  162. <ExportSettingLayout
  163. label="格式"
  164. content={
  165. <Select
  166. defaultValue={format}
  167. bordered={false}
  168. options={[
  169. {
  170. value: "docx",
  171. label: "Word",
  172. },
  173. {
  174. value: "pdf",
  175. label: "PDF",
  176. },
  177. {
  178. value: "epub",
  179. label: "epub电子书",
  180. },
  181. {
  182. value: "markdown",
  183. label: "Markdown",
  184. },
  185. {
  186. value: "html",
  187. label: "Html",
  188. },
  189. {
  190. value: "pptx",
  191. label: "PPT幻灯片",
  192. },
  193. {
  194. value: "txt",
  195. label: "Text纯文本",
  196. },
  197. {
  198. value: "tex",
  199. label: "LaTex",
  200. },
  201. ]}
  202. onSelect={(value) => setFormat(value)}
  203. />
  204. }
  205. />
  206. <ExportSettingLayout
  207. label="原文"
  208. content={
  209. <Switch
  210. disabled={!hasTranslation}
  211. size="small"
  212. defaultChecked={hasOrigin}
  213. onChange={(checked) => setHasOrigin(checked)}
  214. />
  215. }
  216. />
  217. <ExportSettingLayout
  218. label="译文"
  219. content={
  220. <Switch
  221. disabled={!hasOrigin}
  222. size="small"
  223. defaultChecked={hasTranslation}
  224. onChange={(checked) => setHasTranslation(checked)}
  225. />
  226. }
  227. />
  228. <ExportSettingLayout
  229. label="对照方式"
  230. content={
  231. <Select
  232. disabled
  233. defaultValue={"auto"}
  234. bordered={false}
  235. options={[
  236. {
  237. value: "auto",
  238. label: "自动",
  239. },
  240. {
  241. value: "col",
  242. label: "分栏",
  243. },
  244. {
  245. value: "row",
  246. label: "纵列",
  247. },
  248. ]}
  249. onSelect={(value) => setFormat(value)}
  250. />
  251. }
  252. />
  253. <div style={{ display: exportStart ? "block" : "none" }}>
  254. <Progress
  255. percent={exportStatus ? Math.round(exportStatus?.progress * 100) : 0}
  256. status={
  257. exportStatus
  258. ? exportStatus.progress === 1
  259. ? "success"
  260. : "active"
  261. : "normal"
  262. }
  263. />
  264. <Collapse collapsible="icon" ghost>
  265. <Collapse.Panel
  266. header={
  267. <div style={{ display: "flex", justifyContent: "space-between" }}>
  268. <Text>
  269. {exportStatus ? exportStatus.message : "正在生成……"}
  270. </Text>
  271. {url ? (
  272. <a href={url} target="_blank" rel="noreferrer">
  273. {"下载"}
  274. </a>
  275. ) : (
  276. <></>
  277. )}
  278. </div>
  279. }
  280. key="1"
  281. >
  282. <div style={{ height: 200, overflowY: "auto" }}>
  283. {exportStatus?.log?.map((item, id) => {
  284. return <div key={id}>{item}</div>;
  285. })}
  286. </div>
  287. </Collapse.Panel>
  288. </Collapse>
  289. </div>
  290. </Modal>
  291. );
  292. };
  293. export default ExportModalWidget;