export.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import _ from "lodash";
  2. import { saveAs } from "file-saver";
  3. import {
  4. Document,
  5. Packer,
  6. Paragraph,
  7. TextRun,
  8. Table,
  9. TableCell,
  10. TableRow,
  11. WidthType,
  12. } from "docx";
  13. // eslint-disable-next-line
  14. const buildParagraphOptions = (node, context) => {
  15. return _.cond([
  16. [_.matches({ tagName: "H1" }), _.constant({ heading: "Heading1" })],
  17. [_.matches({ tagName: "H2" }), _.constant({ heading: "Heading2" })],
  18. [_.matches({ tagName: "H3" }), _.constant({ heading: "Heading3" })],
  19. [_.matches({ tagName: "H4" }), _.constant({ heading: "Heading4" })],
  20. [_.matches({ tagName: "H5" }), _.constant({ heading: "Heading5" })],
  21. [_.matches({ tagName: "H6" }), _.constant({ heading: "Heading6" })],
  22. [_.stubTrue, _.constant({})],
  23. ])(node);
  24. };
  25. const buildDocxParagraph = (node, context = {}) =>
  26. new Paragraph({
  27. ...buildParagraphOptions(node, context),
  28. children: _.flatMap(node.childNodes, (c) => buildDocxTextRun(c)),
  29. });
  30. // eslint-disable-next-line
  31. const buildTextRunOptions = (node) => {
  32. // TODO 斜体 加粗
  33. return {};
  34. };
  35. // eslint-disable-next-line
  36. const buildDocxTextRun = (node, context = {}) =>
  37. new TextRun({ text: node.textContent.trim(), ...buildTextRunOptions(node) });
  38. // NOTE this make sure root is a `Paragraph`, Paragraph can not nest Paragraph
  39. const buildDocxObject = (node, context = {}) =>
  40. _.cond([
  41. [
  42. _.matches({ nodeType: Node.ELEMENT_NODE }),
  43. _.cond([
  44. [
  45. _.conforms({
  46. tagName: (tag) =>
  47. ["P", "H1", "H2", "H3", "H4", "H5", "H6"].includes(tag),
  48. }),
  49. (c) => buildDocxParagraph(c),
  50. ],
  51. [
  52. _.matches({ tagName: "OL" }),
  53. (n) => _.flatMap(n.childNodes, (c) => buildDocxObject(c, context)),
  54. ],
  55. [
  56. _.matches({ tagName: "UL" }),
  57. (n) => _.flatMap(n.childNodes, (c) => buildDocxObject(c, context)),
  58. ],
  59. [
  60. _.matches({ tagName: "LI" }),
  61. (n) => {
  62. const childNodes = _.filter(
  63. n.childNodes,
  64. (c) => !!c.textContent.trim()
  65. );
  66. let i = _.findIndex(childNodes, (c) =>
  67. ["OL", "UL", "LI"].includes(c.tagName)
  68. );
  69. if (i === -1) {
  70. i = Infinity;
  71. }
  72. const preNodes = _.slice(childNodes, 0, i);
  73. const postNodes = _.slice(childNodes, i);
  74. return [
  75. new Paragraph({
  76. bullet: { level: context.level ?? 0 },
  77. children: _.map(preNodes, (c) => buildDocxTextRun(c)),
  78. }),
  79. ..._.flatMap(postNodes, (c) =>
  80. buildDocxObject(c, {
  81. level: context.level ? context.level + 1 : 1,
  82. })
  83. ),
  84. ];
  85. },
  86. ],
  87. [_.stubTrue, buildDocxParagraph],
  88. ]),
  89. ],
  90. [
  91. _.matches({ nodeType: Node.TEXT_NODE }),
  92. (n) => new Paragraph(n.textContent.trim()),
  93. ],
  94. [_.stubTrue, (n) => new Paragraph(n.textContent.trim())],
  95. ])(node);
  96. const buildFormCardRows = (node) => {
  97. const chunkedFields = _.chunk(_.values(_.get(node, ["content"], {})), 3);
  98. return chunkedFields.map(
  99. (fields) =>
  100. new TableRow({
  101. children: _.flatten(
  102. _.assign(
  103. _.fill(new Array(3), [
  104. new TableCell({ children: [], columnSpan: 2 }),
  105. ]),
  106. fields?.map((field) => {
  107. return [
  108. new TableCell({
  109. children: [new Paragraph(field.label)],
  110. width: { size: 10, type: WidthType.PERCENTAGE },
  111. }),
  112. new TableCell({
  113. children: [new Paragraph(field.value)],
  114. width: { size: 20, type: WidthType.PERCENTAGE },
  115. }),
  116. ];
  117. })
  118. )
  119. ),
  120. })
  121. );
  122. };
  123. const convertAgentContent = (content) => {
  124. const node = document.createElement("div");
  125. node.innerHTML = content;
  126. return _.flatMap(node.childNodes, buildDocxObject);
  127. };
  128. const buildAgentRows = (node) => {
  129. return [
  130. new TableRow({
  131. children: [
  132. new TableCell({
  133. children: [new Paragraph(node.assistantName)],
  134. width: { size: 10, type: WidthType.PERCENTAGE },
  135. }),
  136. new TableCell({
  137. children: convertAgentContent(node.content),
  138. width: { size: 90, type: WidthType.PERCENTAGE },
  139. columnSpan: 5,
  140. }),
  141. ],
  142. }),
  143. ];
  144. };
  145. const buildRows = (nodes) => {
  146. const sectionBuilder = _.cond([
  147. // Form Node
  148. [_.matches({ type: "form_card" }), buildFormCardRows],
  149. // Agent Node
  150. [_.matches({ type: "UserTask" }), buildAgentRows],
  151. [_.stubTrue, _.constant([])],
  152. ]);
  153. return _.flatten(nodes.map(sectionBuilder));
  154. };
  155. export const exportFlowToDocx = async (nodes) => {
  156. const children: unknown[] = [
  157. new Paragraph({
  158. text: "课程设计",
  159. heading: "Heading1",
  160. alignment: "center",
  161. }),
  162. ];
  163. const rows = buildRows(nodes);
  164. if (rows.length) {
  165. children.push(
  166. new Table({
  167. rows,
  168. })
  169. );
  170. }
  171. const doc = new Document({
  172. sections: [
  173. {
  174. children,
  175. },
  176. ],
  177. });
  178. // 将文档打包成Buffer并保存为文件
  179. return await Packer.toBlob(doc)
  180. // (blob) => {
  181. // const mimeType =
  182. // "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
  183. // saveAs(blob, "课程设计.docx", { type: mimeType });
  184. // }
  185. };