/** * GB/T 9704-2012 公文格式常量 * 供所有公文文档生成脚本共享 */ const { AlignmentType, BorderStyle, WidthType, ShadingType, LevelFormat, HeadingLevel, Paragraph, TextRun, Table, TableRow, TableCell, Header, Footer, PageNumber } = require("docx"); // ─── 页面 ───────────────────────────────────────────────── const PAGE = { WIDTH: 11906, // 210mm HEIGHT: 16838, // 297mm MARGIN_TOP: 2098, // 37mm MARGIN_BOTTOM: 1985, // 35mm MARGIN_LEFT: 1588, // 28mm MARGIN_RIGHT: 1474, // 26mm CONTENT_WIDTH: 8844, // 210 - 28 - 26 mm }; // ─── 字体 ───────────────────────────────────────────────── const FONT = { HEITI: { ascii: "SimHei", eastAsia: "黑体", hAnsi: "SimHei" }, FANGSONG: { ascii: "FangSong", eastAsia: "仿宋", hAnsi: "FangSong" }, KAITI: { ascii: "KaiTi", eastAsia: "楷体", hAnsi: "KaiTi" }, SONGTI: { ascii: "SimSun", eastAsia: "宋体", hAnsi: "SimSun" }, }; // ─── 字号(half-point) ─────────────────────────────────── const SIZE = { ER_HAO: 44, // 二号 22pt SAN_HAO: 32, // 三号 16pt SI_HAO: 28, // 四号 14pt XIAO_SI: 24, // 小四 12pt WU_HAO: 21, // 五号 10.5pt }; // ─── 行距(twips = pt × 20) ───────────────────────────── const LINE = { BODY: { line: 560, lineRule: "exact" }, // 28pt HEADING1: { line: 640, lineRule: "exact" }, // 32pt HEADING2: { line: 560, lineRule: "exact" }, // 28pt TABLE: { line: 440, lineRule: "exact" }, // 22pt TITLE: { line: 640, lineRule: "exact" }, // 32pt COMPACT: { line: 360, lineRule: "exact" }, // 18pt }; // ─── 缩进(DXA) ───────────────────────────────────────── const INDENT = { FIRST_LINE: 560, // 首行缩进 2 字符 BULLET_LEFT: 1120, BULLET_HANG: 560, }; // ─── 边框 ───────────────────────────────────────────────── const BORDER = { NONE: { style: BorderStyle.NONE, size: 0 }, THICK: { style: BorderStyle.SINGLE, size: 12, color: "000000" }, // 1.5pt THIN: { style: BorderStyle.SINGLE, size: 6, color: "000000" }, // 0.75pt }; // ─── 样式预设 ───────────────────────────────────────────── function getStyles() { return { default: { document: { run: { font: FONT.FANGSONG, size: SIZE.SI_HAO }, paragraph: { spacing: LINE.BODY }, }, }, paragraphStyles: [ { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true, run: { font: FONT.HEITI, size: SIZE.SAN_HAO, bold: true }, paragraph: { spacing: { ...LINE.HEADING1, before: 240, after: 120 }, outlineLevel: 0 }, }, { id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true, run: { font: FONT.KAITI, size: SIZE.SI_HAO, bold: true }, paragraph: { spacing: { ...LINE.HEADING2, before: 120, after: 60 }, outlineLevel: 1 }, }, ], }; } function getNumbering() { return { config: [{ reference: "bullets", levels: [{ level: 0, format: LevelFormat.BULLET, text: "\u2022", alignment: AlignmentType.LEFT, style: { run: { font: FONT.FANGSONG, size: SIZE.SI_HAO }, paragraph: { indent: { left: INDENT.BULLET_LEFT, hanging: INDENT.BULLET_HANG } }, }, }], }], }; } // ─── 辅助函数 ───────────────────────────────────────────── /** 报告标题区(无封面,居中) */ function titleBlock(title, subtitle, date, institution) { const items = []; items.push(new Paragraph({ alignment: AlignmentType.CENTER, spacing: { ...LINE.TITLE, after: 0 }, children: [new TextRun({ text: title, font: FONT.HEITI, size: SIZE.SAN_HAO, bold: true })], })); if (subtitle) { items.push(new Paragraph({ alignment: AlignmentType.CENTER, spacing: { ...LINE.TITLE, after: 120 }, children: [new TextRun({ text: subtitle, font: FONT.HEITI, size: SIZE.SAN_HAO })], })); } items.push(new Paragraph({ alignment: AlignmentType.CENTER, spacing: { ...LINE.BODY, after: 0 }, children: [new TextRun({ text: date, font: FONT.FANGSONG, size: SIZE.SI_HAO })], })); if (institution) { items.push(new Paragraph({ alignment: AlignmentType.CENTER, spacing: { ...LINE.BODY, after: 240 }, children: [new TextRun({ text: institution, font: FONT.FANGSONG, size: SIZE.SI_HAO })], })); } return items; } /** 一级标题 */ function h1(text) { return new Paragraph({ heading: HeadingLevel.HEADING_1, spacing: { ...LINE.HEADING1, before: 240, after: 120 }, children: [new TextRun({ text, font: FONT.HEITI, size: SIZE.SAN_HAO, bold: true })], }); } /** 二级标题 */ function h2(text) { return new Paragraph({ heading: HeadingLevel.HEADING_2, spacing: { ...LINE.HEADING2, before: 120, after: 60 }, children: [new TextRun({ text, font: FONT.KAITI, size: SIZE.SI_HAO, bold: true })], }); } /** 正文段落 */ function body(text, opts = {}) { const { bold = false, align = AlignmentType.JUSTIFIED, indent = true } = opts; return new Paragraph({ alignment: align, indent: indent ? { firstLine: INDENT.FIRST_LINE } : undefined, spacing: LINE.BODY, children: [new TextRun({ text, font: FONT.FANGSONG, size: SIZE.SI_HAO, bold })], }); } /** 多 run 正文段落(支持混排加粗) */ function bodyRuns(runs, opts = {}) { const { align = AlignmentType.JUSTIFIED, indent = true } = opts; return new Paragraph({ alignment: align, indent: indent ? { firstLine: INDENT.FIRST_LINE } : undefined, spacing: LINE.BODY, children: runs.map(r => { if (typeof r === "string") { return new TextRun({ text: r, font: FONT.FANGSONG, size: SIZE.SI_HAO }); } return new TextRun({ font: FONT.FANGSONG, size: SIZE.SI_HAO, ...r }); }), }); } /** 列表项 */ function bullet(text) { return new Paragraph({ numbering: { reference: "bullets", level: 0 }, spacing: LINE.BODY, children: [new TextRun({ text, font: FONT.FANGSONG, size: SIZE.SI_HAO })], }); } /** 三线表 */ function threeLineTable(headers, rows, opts = {}) { const { colWidths } = opts; const widths = colWidths || headers.map(() => Math.floor(PAGE.CONTENT_WIDTH / headers.length)); function cell(text, isHeader, isLastRow, colIdx) { const borders = { top: isHeader ? BORDER.THICK : BORDER.NONE, bottom: isHeader ? BORDER.THIN : (isLastRow ? BORDER.THICK : BORDER.NONE), left: BORDER.NONE, right: BORDER.NONE, }; return new TableCell({ borders, width: { size: widths[colIdx], type: WidthType.DXA }, shading: isHeader ? { fill: "1F3864", type: ShadingType.CLEAR } : undefined, margins: { top: 40, bottom: 40, left: 80, right: 80 }, children: [new Paragraph({ alignment: AlignmentType.CENTER, spacing: LINE.TABLE, children: [new TextRun({ text: String(text), font: isHeader ? FONT.HEITI : FONT.FANGSONG, size: SIZE.XIAO_SI, bold: isHeader, color: isHeader ? "FFFFFF" : undefined, })], })], }); } // 表级边框全部 NONE,由 cell 级别控制三线效果 const noBorder = { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }; return new Table({ width: { size: PAGE.CONTENT_WIDTH, type: WidthType.DXA }, columnWidths: widths, borders: { top: noBorder, bottom: noBorder, left: noBorder, right: noBorder, insideHorizontal: noBorder, insideVertical: noBorder, }, rows: [ new TableRow({ children: headers.map((h, i) => cell(h, true, false, i)) }), ...rows.map((row, ri) => new TableRow({ children: row.map((c, ci) => cell(c, false, ri === rows.length - 1, ci)), }) ), ], }); } /** 标准页脚(— 页码 —) */ function pageFooter() { return new Footer({ children: [new Paragraph({ alignment: AlignmentType.CENTER, children: [ new TextRun({ text: "— ", font: FONT.FANGSONG, size: SIZE.WU_HAO }), new TextRun({ children: [PageNumber.CURRENT], font: FONT.FANGSONG, size: SIZE.WU_HAO }), new TextRun({ text: " —", font: FONT.FANGSONG, size: SIZE.WU_HAO }), ], })], }); } /** 标准页眉 */ function pageHeader(text = "内部资料 注意保密") { return new Header({ children: [new Paragraph({ alignment: AlignmentType.CENTER, children: [new TextRun({ text, font: FONT.FANGSONG, size: SIZE.WU_HAO, color: "808080", })], })], }); } /** section 属性(页面设置) */ function sectionProps(opts = {}) { return { page: { size: { width: PAGE.WIDTH, height: PAGE.HEIGHT }, margin: { top: PAGE.MARGIN_TOP, bottom: PAGE.MARGIN_BOTTOM, left: PAGE.MARGIN_LEFT, right: PAGE.MARGIN_RIGHT, }, }, ...opts, }; } module.exports = { PAGE, FONT, SIZE, LINE, INDENT, BORDER, getStyles, getNumbering, sectionProps, titleBlock, h1, h2, body, bodyRuns, bullet, threeLineTable, pageFooter, pageHeader, };