builder.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. import { useState } from "react";
  2. import styles from "../styles/Builder.module.css";
  3. interface IMap {
  4. [key: string]: boolean;
  5. }
  6. const pluginList = ["Sort", "Filter", "Comment", "Alternating Colors"];
  7. // TODO 选项匹配插件名称
  8. // TODO 代码包含一个完整的运行案例,直接运行,或者npm i 后运行,README.md引导
  9. // TODO 下载代码压缩文件
  10. // TODO 下载完成之后,服务器新复制的项目清除掉
  11. // TODO 测试并发能力
  12. function Builder() {
  13. const pluginMap: IMap = {};
  14. const [msgList, setMsgList] = useState<string[]>([]);
  15. const [progressValue, setProgressValue] = useState<number>(0);
  16. const [buildId, setBuildId] = useState<string>("");
  17. // const
  18. const check = (e: React.ChangeEvent<HTMLInputElement>) => {
  19. pluginMap[e.target.value] = e.target.checked;
  20. };
  21. const build = () => {
  22. const buildList = [];
  23. for (const plugin in pluginMap) {
  24. if (pluginMap[plugin] === true) {
  25. buildList.push(plugin);
  26. }
  27. }
  28. postData("/api/build", { buildList }).then((data) => {
  29. setBuildId(data.buildId);
  30. startBuild(data.buildId);
  31. });
  32. };
  33. const download = () => {
  34. fetch("/api/download?buildId=" + buildId, {
  35. method: "GET", // *GET, POST, PUT, DELETE, etc.
  36. mode: "cors", // no-cors, *cors, same-origin
  37. cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
  38. credentials: "same-origin", // include, *same-origin, omit
  39. headers: {
  40. "Content-Type": "application/octet-stream",
  41. // 'Content-Type': 'application/x-www-form-urlencoded',
  42. },
  43. redirect: "follow", // manual, *follow, error
  44. referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
  45. }).then ((res) => res.blob()).then((blob)=>{
  46. downloadFile(blob,'luckysheet-custom-build.zip')
  47. })
  48. };
  49. /**
  50. * reference: https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API/Using_Fetch
  51. * @param url
  52. * @param data
  53. * @returns
  54. */
  55. async function postData(url = "", data = {}) {
  56. // Default options are marked with *
  57. const response = await fetch(url, {
  58. method: "POST", // *GET, POST, PUT, DELETE, etc.
  59. mode: "cors", // no-cors, *cors, same-origin
  60. cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
  61. credentials: "same-origin", // include, *same-origin, omit
  62. headers: {
  63. "Content-Type": "application/json",
  64. // 'Content-Type': 'application/x-www-form-urlencoded',
  65. },
  66. redirect: "follow", // manual, *follow, error
  67. referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
  68. body: JSON.stringify(data), // body data type must match "Content-Type" header
  69. });
  70. return response.json(); // parses JSON response into native JavaScript objects
  71. }
  72. async function getFile(url = "") {
  73. // Default options are marked with *
  74. const response = await fetch(url, {
  75. method: "GET", // *GET, POST, PUT, DELETE, etc.
  76. mode: "cors", // no-cors, *cors, same-origin
  77. cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
  78. credentials: "same-origin", // include, *same-origin, omit
  79. headers: {
  80. "Content-Type": "application/octet-stream",
  81. // 'Content-Type': 'application/x-www-form-urlencoded',
  82. },
  83. redirect: "follow", // manual, *follow, error
  84. referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
  85. });
  86. return response; // parses JSON response into native JavaScript objects
  87. }
  88. function startBuild(buildId: string) {
  89. // when "Start" button pressed
  90. if (!window.EventSource) {
  91. // IE or an old browser
  92. alert("The browser doesn't support EventSource.");
  93. return;
  94. }
  95. const eventSource = new EventSource("/api/progress?buildId=" + buildId);
  96. let retryCount = 0;
  97. eventSource.onopen = function (e) {
  98. console.log("Event: open");
  99. };
  100. eventSource.onerror = function (this: EventSource) {
  101. console.log(`Event: readyState=${this.readyState})`);
  102. if (this.readyState == EventSource.CLOSED) {
  103. console.log("Event: Connection was closed.");
  104. } else if (this.readyState == EventSource.CONNECTING) {
  105. eventSource.close();
  106. console.log("Event: Connection was closed.");
  107. // if (++retryCount === 5) {
  108. // eventSource.close();
  109. // }
  110. } else {
  111. console.log("Event: Error has occured.");
  112. }
  113. };
  114. eventSource.addEventListener("progress", function (e) {
  115. const dataList = e.data.split("-");
  116. const msg = dataList[0];
  117. const progress = dataList[1];
  118. setMsgList((msgList) => [...msgList, msg]);
  119. setProgressValue(progress * 100);
  120. if (msg === "Success" || msg === "Fail") {
  121. eventSource.close();
  122. }
  123. });
  124. }
  125. /**
  126. * 下载文件
  127. * @param content 文件流
  128. * @param fileName 文件名称
  129. */
  130. function downloadFile(blob: any, fileName: string) {
  131. // const blob = new Blob([content], {
  132. // type: 'application/octet-stream'
  133. // });
  134. const a = document.createElement("a");
  135. const url = window.URL.createObjectURL(blob);
  136. a.href = url;
  137. a.download = fileName;
  138. document.body.appendChild(a); // we need to append the element to the dom -> otherwise it will not work in firefox
  139. a.click();
  140. window.URL.revokeObjectURL(url);
  141. a.remove(); //afterwards we remove the element again
  142. }
  143. return (
  144. <div className={styles.container}>
  145. <h2>Luckysheet Custom Builder</h2>
  146. <div className="container">
  147. <h3>Choose plugins</h3>
  148. <div>
  149. {pluginList.map((plugin) => {
  150. return (
  151. <p key={plugin}>
  152. <input
  153. type="checkbox"
  154. id={plugin}
  155. value={plugin}
  156. onChange={check}
  157. ></input>
  158. <label htmlFor={plugin}>{plugin}</label>
  159. </p>
  160. );
  161. })}
  162. </div>
  163. <div className={styles.operate}>
  164. <button onClick={build}>build</button>
  165. </div>
  166. <div>
  167. {progressValue === 0 ? (
  168. ""
  169. ) : (
  170. <div>
  171. <progress
  172. id="progress-bar"
  173. max="100"
  174. value={progressValue}
  175. ></progress>
  176. <span>{progressValue}%</span>
  177. </div>
  178. )}
  179. <div>
  180. <h3>Build Log</h3>
  181. <ul>
  182. {msgList.map((msg, i) => (
  183. <li key={i}>{msg}</li>
  184. ))}
  185. </ul>
  186. </div>
  187. <div>
  188. {/* <button onClick={download}>download</button> */}
  189. {progressValue === 100 ? (
  190. <button onClick={download}>download</button>
  191. ) : (
  192. ""
  193. )}
  194. </div>
  195. </div>
  196. </div>
  197. </div>
  198. );
  199. }
  200. export default Builder;