From ef199920c1cd54acfea8d39fb32dc0308f038cae Mon Sep 17 00:00:00 2001
From: yuan <123@>
Date: 星期五, 09 十月 2026 14:47:57 +0800
Subject: [PATCH] fix: 库存报表入库时间不展示时分秒

---
 src/utils/docxWriter.js |  317 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 317 insertions(+), 0 deletions(-)

diff --git a/src/utils/docxWriter.js b/src/utils/docxWriter.js
new file mode 100644
index 0000000..96cced5
--- /dev/null
+++ b/src/utils/docxWriter.js
@@ -0,0 +1,317 @@
+/**
+ * 鏋佺畝 DOCX 鐢熸垚鍣紙闆朵緷璧栵級
+ *
+ * 鍙疄鐜板鍑哄崟鎹墍闇�鐨� WordprocessingML 瀛愰泦锛氶〉闈㈠昂瀵�/椤佃竟璺濄�佹钀�
+ * 锛堢缉杩涖�佽璺濄�佸埗琛ㄤ綅銆佸榻愩�佸瓧浣撳瓧鍙峰姞绮楋級銆佽〃鏍硷紙鍥哄畾鍒楀銆佸悎骞跺崟鍏冩牸銆�
+ * 杈规銆佽楂樸�佸崟鍏冩牸鍨傜洿瀵归綈锛夈��
+ *
+ * 鏂囨。妯″瀷锛�
+ *   buildDocxBlob({
+ *     page: { width, height, margin: { top, right, bottom, left } },
+ *     blocks: [
+ *       { type: "paragraph", indentLeft, spacing: { before, line, lineRule },
+ *         align, tabs: [2310], runs: [{ text, size, bold }, { tab: true }] },
+ *       { type: "table", width, indent, grid: [821, ...],
+ *         rows: [{ height, heightRule, cells: [
+ *           { text, width, gridSpan, align, bold, size, vAlign } ] }] },
+ *     ],
+ *   })
+ *
+ * 灏哄鍗曚綅缁熶竴浣跨敤 twips锛�1/20 纾咃級锛屽瓧鍙蜂娇鐢ㄥ崐纾咃紙sz=21 鍗� 10.5 纾咃級銆�
+ */
+import { createZip, utf8 } from "./zipStore";
+
+const DOCX_MIME =
+  "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
+const W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
+const R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
+
+const escapeXml = (value) =>
+  String(value)
+    .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, "")
+    .replace(/&/g, "&amp;")
+    .replace(/</g, "&lt;")
+    .replace(/>/g, "&gt;")
+    .replace(/"/g, "&quot;")
+    .replace(/'/g, "&apos;");
+
+const ATTR_ORDER = ["val", "color", "sz", "space", "w", "type", "before", "after", "line", "lineRule", "left", "right", "firstLine", "hanging"];
+
+const el = (tag, attrs, children) => {
+  const keys = attrs ? Object.keys(attrs).filter((k) => attrs[k] !== undefined && attrs[k] !== null) : [];
+  keys.sort((a, b) => {
+    const ia = ATTR_ORDER.indexOf(a);
+    const ib = ATTR_ORDER.indexOf(b);
+    return (ia < 0 ? 99 : ia) - (ib < 0 ? 99 : ib);
+  });
+  const attrText = keys.map((k) => ` w:${k}="${escapeXml(attrs[k])}"`).join("");
+  if (children === undefined) return `<w:${tag}${attrText}/>`;
+  return `<w:${tag}${attrText}>${children}</w:${tag}>`;
+};
+
+/** 鐢熸垚 run 鐨勫睘鎬ц妭鐐癸紙椤哄簭蹇呴』绗﹀悎 CT_RPr 鐨� schema 瀹氫箟锛� */
+const buildRunProps = (run) => {
+  const parts = [];
+  if (run.font) {
+    parts.push(
+      el("rFonts", { ascii: run.font, hAnsi: run.font, eastAsia: run.font, cs: run.font })
+    );
+  }
+  if (run.bold) {
+    parts.push(el("b"), el("bCs"));
+  }
+  if (run.color) parts.push(el("color", { val: run.color }));
+  if (run.size) {
+    const sz = String(run.size);
+    parts.push(el("sz", { val: sz }), el("szCs", { val: sz }));
+  }
+  return parts.length ? el("rPr", null, parts.join("")) : "";
+};
+
+const buildRuns = (runs = []) =>
+  runs
+    .map((run) => {
+      const rPr = buildRunProps(run);
+      if (run.tab) return `<w:r>${rPr}<w:tab/></w:r>`;
+      const text = run.text === undefined || run.text === null ? "" : String(run.text);
+      if (!text) return rPr ? `<w:r>${rPr}</w:r>` : "";
+      return `<w:r>${rPr}<w:t xml:space="preserve">${escapeXml(text)}</w:t></w:r>`;
+    })
+    .join("");
+
+/** 鐢熸垚娈佃惤灞炴�ц妭鐐癸紝瀛愬厓绱犻『搴忓繀椤荤鍚� CT_PPr */
+const buildParaProps = (block) => {
+  const parts = [];
+  if (block.tabs && block.tabs.length) {
+    parts.push(
+      el(
+        "tabs",
+        null,
+        block.tabs.map((pos) => el("tab", { val: "left", pos: String(pos) })).join("")
+      )
+    );
+  }
+  if (block.spacing) {
+    parts.push(
+      el("spacing", {
+        before: block.spacing.before,
+        after: block.spacing.after,
+        line: block.spacing.line,
+        lineRule: block.spacing.lineRule,
+      })
+    );
+  }
+  if (block.indentLeft || block.indentRight || block.indentFirstLine) {
+    parts.push(
+      el("ind", {
+        left: block.indentLeft,
+        right: block.indentRight,
+        firstLine: block.indentFirstLine,
+      })
+    );
+  }
+  if (block.align) parts.push(el("jc", { val: block.align }));
+  if (block.runs && block.runs.length) {
+    const rPr = buildRunProps(block.runs[0]);
+    if (rPr) parts.push(rPr);
+  }
+  return parts.length ? el("pPr", null, parts.join("")) : "";
+};
+
+const buildParagraph = (block) => {
+  if (block.type === "empty") return "<w:p/>";
+  return `<w:p>${buildParaProps(block)}${buildRuns(block.runs)}</w:p>`;
+};
+
+const BORDER_SIDES = ["top", "left", "bottom", "right", "insideH", "insideV"];
+
+const buildTableBorders = (border) => {
+  if (!border) return "";
+  const sides = BORDER_SIDES.map((side) => {
+    if (!border[side]) return "";
+    return el(side, {
+      val: border[side].val || "single",
+      color: border[side].color || "000000",
+      sz: String(border[side].sz || 6),
+      space: "0",
+    });
+  }).join("");
+  return sides ? el("tblBorders", null, sides) : "";
+};
+
+const buildCell = (cell) => {
+  const parts = [];
+  if (cell.width) parts.push(el("tcW", { w: String(cell.width), type: "dxa" }));
+  if (cell.gridSpan && cell.gridSpan > 1) {
+    parts.push(el("gridSpan", { val: String(cell.gridSpan) }));
+  }
+  if (cell.vAlign) parts.push(el("vAlign", { val: cell.vAlign }));
+
+  // 绌哄崟鍏冩牸涔熶繚鐣欐钀藉睘鎬э紙瀵归綈/缂╄繘锛夛紝渚夸簬鍚庣画鍦� Word 涓户缁紪杈�
+  const paragraph = buildParagraph({
+    align: cell.align,
+    indentLeft: cell.indentLeft,
+    spacing: cell.spacing,
+    runs: cell.text
+      ? [{ text: cell.text, size: cell.size, bold: cell.bold, font: cell.font }]
+      : [],
+  });
+
+  return `<w:tc>${parts.length ? el("tcPr", null, parts.join("")) : ""}${paragraph}</w:tc>`;
+};
+
+const buildTable = (table) => {
+  const tblPrParts = [];
+  if (table.width) tblPrParts.push(el("tblW", { w: String(table.width), type: "dxa" }));
+  if (table.indent) tblPrParts.push(el("tblInd", { w: String(table.indent), type: "dxa" }));
+  const borders = buildTableBorders(table.border);
+  if (borders) tblPrParts.push(borders);
+  tblPrParts.push(el("tblLayout", { type: "fixed" }));
+  if (table.cellMargin !== undefined && table.cellMargin !== null) {
+    const m = table.cellMargin;
+    tblPrParts.push(
+      el(
+        "tblCellMar",
+        null,
+        ["top", "left", "bottom", "right"]
+          .map((side) => el(side, { w: String(m), type: "dxa" }))
+          .join("")
+      )
+    );
+  }
+
+  const grid = (table.grid || [])
+    .map((w) => el("gridCol", { w: String(w) }))
+    .join("");
+
+  const rows = (table.rows || [])
+    .map((row) => {
+      const trPr = row.height
+        ? el("trPr", null, el("trHeight", { val: String(row.height), hRule: row.heightRule || "atLeast" }))
+        : "";
+      const cells = (row.cells || []).map(buildCell).join("");
+      return `<w:tr>${trPr}${cells}</w:tr>`;
+    })
+    .join("");
+
+  return `<w:tbl>${el("tblPr", null, tblPrParts.join(""))}${el(
+    "tblGrid",
+    null,
+    grid
+  )}${rows}</w:tbl>`;
+};
+
+const buildSectPr = (page) => {
+  const margin = page.margin || {};
+  return el(
+    "sectPr",
+    null,
+    [
+      el("pgSz", { w: String(page.width), h: String(page.height) }),
+      el("pgMar", {
+        top: String(margin.top || 0),
+        right: String(margin.right || 0),
+        bottom: String(margin.bottom || 0),
+        left: String(margin.left || 0),
+        header: "0",
+        footer: "0",
+        gutter: "0",
+      }),
+      el("cols", { space: "720", num: "1" }),
+    ].join("")
+  );
+};
+
+const buildDocumentXml = (model) => {
+  const body = (model.blocks || [])
+    .map((block) => (block.type === "table" ? buildTable(block) : buildParagraph(block)))
+    .join("");
+  return [
+    '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>',
+    `<w:document xmlns:w="${W_NS}" xmlns:r="${R_NS}">`,
+    `<w:body>${body}${buildSectPr(model.page || {})}</w:body>`,
+    "</w:document>",
+  ].join("");
+};
+
+const buildStylesXml = (defaultFont, defaultSize) => {
+  const fonts = el("rFonts", {
+    ascii: defaultFont,
+    hAnsi: defaultFont,
+    eastAsia: defaultFont,
+    cs: defaultFont,
+  });
+  const sz = String(defaultSize);
+  return [
+    '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>',
+    `<w:styles xmlns:w="${W_NS}">`,
+    el(
+      "docDefaults",
+      null,
+      el(
+        "rPrDefault",
+        null,
+        el("rPr", null, `${fonts}${el("sz", { val: sz })}${el("szCs", { val: sz })}`)
+      ) +
+        el(
+          "pPrDefault",
+          null,
+          el("pPr", null, el("spacing", { line: "240", lineRule: "auto" }))
+        )
+    ),
+    '<w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/><w:qFormat/></w:style>',
+    "</w:styles>",
+  ].join("");
+};
+
+/**
+ * 鐢熸垚 docx 鏂囦欢鐨� Blob銆�
+ * @param {{page?: object, blocks?: Array, defaultFont?: string, defaultSize?: number}} model
+ * @returns {Blob}
+ */
+export function buildDocxBlob(model = {}) {
+  const font = model.defaultFont || "瀹嬩綋";
+  const size = model.defaultSize || 21;
+
+  const contentTypes = [
+    '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>',
+    '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">',
+    '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>',
+    '<Default Extension="xml" ContentType="application/xml"/>',
+    '<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>',
+    '<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>',
+    "</Types>",
+  ].join("");
+
+  const files = [
+    { name: "[Content_Types].xml", data: utf8(contentTypes) },
+    {
+      name: "_rels/.rels",
+      data: utf8(
+        [
+          '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>',
+          '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">',
+          `<Relationship Id="rId1" Type="${R_NS}/officeDocument" Target="word/document.xml"/>`,
+          "</Relationships>",
+        ].join("")
+      ),
+    },
+    { name: "word/document.xml", data: utf8(buildDocumentXml(model)) },
+    {
+      name: "word/_rels/document.xml.rels",
+      data: utf8(
+        [
+          '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>',
+          '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">',
+          `<Relationship Id="rId1" Type="${R_NS}/styles" Target="styles.xml"/>`,
+          "</Relationships>",
+        ].join("")
+      ),
+    },
+    { name: "word/styles.xml", data: utf8(buildStylesXml(font, size)) },
+  ];
+
+  return createZip(files, DOCX_MIME);
+}
+
+export default buildDocxBlob;

--
Gitblit v1.9.3