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, "&")
+ .replace(/</g, "<")
+ .replace(/>/g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+
+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