copyFileCsv.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import fs from "fs-extra";
  2. import path from "path";
  3. import PizZip from "pizzip";
  4. import Docxtemplater from "docxtemplater";
  5. import { fileURLToPath } from "url";
  6. import ImageModule from "docxtemplater-image-module-free";
  7. import axios from "axios";
  8. import sizeOf from "image-size";
  9. const __filename = fileURLToPath(import.meta.url);
  10. const __dirname = path.dirname(__filename);
  11. // 读取本地或远程图片,返回 Buffer
  12. const getImageFromUrl = async (url) => {
  13. if (typeof url !== "string") {
  14. throw new TypeError(`图片 URL 应为字符串,但实际是 ${typeof url}`);
  15. }
  16. if (url.startsWith("http")) {
  17. const safeUrl = url.replace(/#/g, "%23"); // 处理 URL 中的 #
  18. const response = await axios.get(safeUrl, { responseType: "arraybuffer" });
  19. const buffer = Buffer.from(response.data, "binary");
  20. if (buffer.length < 1000) {
  21. console.warn(`⚠️ 图像可能无效或过小: ${url}`);
  22. }
  23. return buffer;
  24. } else {
  25. return fs.readFileSync(url); // 本地图片
  26. }
  27. };
  28. // 预加载所有图片为 Buffer
  29. const preloadAllImages = async (chartsImages) => {
  30. const allUrls = Object.values(chartsImages).flat(); // 扁平化为图片 URL 列表
  31. const imageMap = {};
  32. for (const url of allUrls) {
  33. if (typeof url !== "string" || !url.trim()) {
  34. console.warn("⚠️ 非法图片 URL:", url);
  35. continue;
  36. }
  37. try {
  38. const buffer = await getImageFromUrl(url);
  39. if (!buffer || buffer.length === 0) {
  40. console.warn("⚠️ 获取的图片 buffer 长度为 0:", url);
  41. continue;
  42. }
  43. imageMap[url] = buffer;
  44. } catch (e) {
  45. console.warn("⚠️ 加载图片失败:", url, e.message);
  46. }
  47. }
  48. return imageMap;
  49. };
  50. // 生成 Word 文件
  51. export const copyFileDocx = async (fieldInfo, chartsImages) => {
  52. try {
  53. const sourceFilePath = path.join(
  54. process.cwd(),
  55. "src",
  56. "public",
  57. "file",
  58. "副本XXX风电场可靠性和能效双提升数据分析报告(模板).docx"
  59. );
  60. const targetFilePath = path.join(
  61. process.cwd(),
  62. "src",
  63. "public",
  64. "file",
  65. `${fieldInfo.companyName}${fieldInfo.fieldName}可靠性和能效双提升数据分析报告.docx`
  66. );
  67. // 拷贝模板
  68. await fs.copy(sourceFilePath, targetFilePath);
  69. const content = await fs.readFile(targetFilePath, "binary");
  70. const zip = new PizZip(content);
  71. // 预加载所有图片
  72. const imageBufferMap = await preloadAllImages(chartsImages);
  73. // 配置 image module
  74. const imageModule = new ImageModule({
  75. centered: true,
  76. getImage: (tagValue) => {
  77. const buffer = imageBufferMap[tagValue];
  78. if (!buffer) {
  79. throw new Error(`未找到图片 Buffer: ${tagValue}`);
  80. }
  81. return buffer;
  82. },
  83. getSize: (imgBuffer) => {
  84. try {
  85. const dimensions = sizeOf(imgBuffer);
  86. const maxWidth = 500;
  87. const ratio = maxWidth / dimensions.width;
  88. return [maxWidth, dimensions.height * ratio];
  89. } catch (e) {
  90. console.error("❌ 获取图片尺寸失败", e);
  91. console.error("buffer 长度:", imgBuffer.length);
  92. throw new Error("图片尺寸获取失败,可能是非法图片或 Buffer 损坏");
  93. }
  94. },
  95. });
  96. const doc = new Docxtemplater(zip, {
  97. paragraphLoop: true,
  98. linebreaks: true,
  99. modules: [imageModule],
  100. });
  101. const now = new Date();
  102. const year = now.getFullYear();
  103. const month = String(now.getMonth() + 1).padStart(2, "0");
  104. // 构建模板图像字段对象
  105. const imageTagData = {};
  106. for (const [tag, urlList] of Object.entries(chartsImages)) {
  107. if (Array.isArray(urlList)) {
  108. imageTagData[tag] = urlList.map((url) => ({ image: url }));
  109. } else {
  110. console.warn(`⚠️ 无效的 urlList: ${tag}`, urlList);
  111. imageTagData[tag] = []; // 或 throw error
  112. }
  113. }
  114. console.log(chartsImages.rows, "chartsImages");
  115. const renderData = {
  116. Year_now: year,
  117. Month_now: month,
  118. Province: fieldInfo.provinceName,
  119. Wind_farm: fieldInfo.fieldName,
  120. Wind_turbine: fieldInfo.engineMillTypes.join(","),
  121. Overview_of_the_Wind_Farm: `${fieldInfo.fieldName}位于${
  122. fieldInfo.provinceName
  123. }${fieldInfo.cityName},海拔高度为${fieldInfo.elevationHeight} 米,经度${
  124. fieldInfo.longitude
  125. }°,纬度${fieldInfo.latitude}°。风场总容量为${
  126. fieldInfo.ratedCapacityNumber
  127. } MW,共安装${
  128. fieldInfo.engineCount
  129. } 台风机,机型包括${fieldInfo.engineMillTypes?.join(",")}`,
  130. ...imageTagData, // 图像数据动态插入
  131. rows: chartsImages.rows,
  132. windTurbineRows: chartsImages.windTurbineRows,
  133. faultRows: chartsImages.faultRows,
  134. yawErrorRows: chartsImages.yawErrorRows,
  135. };
  136. // 渲染 Word 模板
  137. doc.render(renderData);
  138. // 保存生成的文档
  139. const buffer = doc.getZip().generate({ type: "nodebuffer" });
  140. await fs.writeFile(targetFilePath, buffer);
  141. return {
  142. url: targetFilePath,
  143. message: `✅ 文档生成成功:${targetFilePath}`,
  144. };
  145. } catch (error) {
  146. console.error("❌ 生成 Word 文档失败:", error);
  147. throw error;
  148. }
  149. };