288 lines
9.9 KiB
JavaScript
288 lines
9.9 KiB
JavaScript
|
|
/**
|
|||
|
|
* 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,
|
|||
|
|
};
|