skills/docx-cn/scripts/gongwen_constants.js

288 lines
9.9 KiB
JavaScript
Raw Permalink Normal View History

/**
* 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,
};