前端Word導(dǎo)出自定義頁眉表格實(shí)現(xiàn)方案的完整流程
前言
在前端導(dǎo)出 Word 文檔的場景中,自定義頁眉(尤其是包含復(fù)雜表格布局的頁眉)是常見需求。本文將聚焦標(biāo)準(zhǔn) JD 庫項(xiàng)目中,如何通過docx庫實(shí)現(xiàn)固定結(jié)構(gòu)的自定義頁眉表格,結(jié)合實(shí)際代碼,拆解從表格布局設(shè)計(jì)、樣式配置到最終集成的完整流程。
一、需求核心:自定義頁眉表格的目標(biāo)
本次導(dǎo)出功能的頁眉表格需滿足以下要求:
- 布局:4 列 2 行結(jié)構(gòu),第二列合并兩行顯示核心信息
- 內(nèi)容:包含標(biāo)識文本(VOSS、Job Description)、職位名稱、版本信息(Issue No.)、日期(Issue Date)、編制人(Compiled By)、審批人(Approved By)
- 樣式:統(tǒng)一邊框、文字居中對齊、固定列寬比例、垂直居中布局
- 適配:頁眉在文檔每一頁頂部顯示,與正文內(nèi)容分離
二、技術(shù)選型與依賴準(zhǔn)備
1. 核心依賴
docx@9.5.1:負(fù)責(zé) Word 文檔結(jié)構(gòu)構(gòu)建、表格和樣式定義(選擇該版本是因?yàn)?API 穩(wěn)定,適配前端動態(tài)導(dǎo)入)file-saver:處理 Blob 對象轉(zhuǎn)文件下載
2. 依賴安裝
npm install docx@9.5.1 file-saver --save
3. 動態(tài)導(dǎo)入優(yōu)化
為避免初始打包體積過大,在導(dǎo)出函數(shù)內(nèi)動態(tài)導(dǎo)入docx庫:
const handleExportJd = async () => {
try {
// 動態(tài)導(dǎo)入docx核心模塊(僅導(dǎo)出時(shí)加載)
const docx = await import('docx');
const {
Table, TableRow, TableCell, TextRun, Paragraph,
WidthType, BorderStyle, AlignmentType, Header
} = docx;
// 后續(xù)頁眉表格構(gòu)建邏輯...
} catch (error) {
console.error('導(dǎo)出Word失敗:', error);
antdApp.message.error('導(dǎo)出Word失敗,請檢查文檔內(nèi)容格式');
}
};三、核心組件(DOM)
<div className={styles.rightPane}>
<Typography.Title className={styles.JdTitle} level={5}>
<span className={styles.JdBasTitleText}>基礎(chǔ)信息</span>
<div className={styles.JdButton}>
<Button className={styles.JdExportButton} onClick={handleExportJd} disabled={!selectedJdDescription}><img src="https://xingge-ai.oss-cn-shenzhen.aliyuncs.com/fusi/down_icon.png" alt="導(dǎo)出JD" width={14} height={14} />導(dǎo)出JD</Button>
</div>
</Typography.Title>
<div className={styles.infoContainer}>
<div className={styles.infoRow}>
<div className={styles.infoItem}>
<span className={styles.infoLabel}>Issue No.</span>
<Input value="001-F002" disabled style={{ width: 150, marginLeft: 10 }} />
</div>
<div className={styles.infoItem}>
<span className={styles.infoLabel}>Issue Date.</span>
<Input value="2024/9/21" disabled style={{ width: 150, marginLeft: 10 }} />
</div>
</div>
<div className={styles.infoRow}>
<div className={styles.infoItem}>
<span className={styles.infoLabel}>Compiled By</span>
<Input value={compiledBy} onChange={(e) => setCompiledBy(e.target.value)} style={{ width: 150, marginLeft: 10 }} />
</div>
<div className={styles.infoItem}>
<span className={styles.infoLabel}>Approved By</span>
<Input value={approvedBy} onChange={(e) => setApprovedBy(e.target.value)} style={{ width: 150, marginLeft: 10 }} />
</div>
</div>
</div>
<Typography.Title className={styles.JdTitle} level={5}>
<span className={styles.JdTitleText}>JD描述</span>
</Typography.Title>
<Spin spinning={rowLoading}>
<Input.TextArea
value={selectedJdDescription}
onChange={(e) => setSelectedJdDescription(e.target.value)}
autoSize={{ minRows: 27 }}
placeholder="暫無JD描述"
/>
</Spin>
</div>其中 selectedJdDescription 的內(nèi)容是根據(jù)左側(cè)點(diǎn)擊表格行 -> 調(diào)用 detail 接口獲取 documentId/segmentId,再請求子塊內(nèi)容(獲取接口中 content 的 Markdown 內(nèi)容 ---正文內(nèi)容)
四、自定義頁眉表格的核心實(shí)現(xiàn)
1. 表格結(jié)構(gòu)設(shè)計(jì)(4 列 2 行)
先明確表格的布局規(guī)則,確保各列寬比例合理、內(nèi)容分區(qū)清晰:
| 列索引(寬比例) | 第 1 行內(nèi)容 | 第 2 行內(nèi)容 | 特殊配置 |
|---|---|---|---|
| 列 1(25%) | VOSS(標(biāo)識文本) | Job Description + 職位描述 | 獨(dú)立兩行 |
| 列 2(35%) | 職位名稱(從選中 JD 提?。?/td> | - | 合并兩行(rowSpan=2) |
| 列 3(20%) | Issue No. + 編號(001-F002) | Compiled By + 輸入值 | 獨(dú)立兩行 |
| 列 4(20%) | Issue Date + 日期(2024/9/21) | Approved By + 輸入值 | 獨(dú)立兩行 |
2. 關(guān)鍵配置:表格樣式與布局約束
2.1 統(tǒng)一樣式定義(復(fù)用性優(yōu)化)
提前定義表格邊框、文字大小、對齊方式等公共樣式,避免重復(fù)代碼:
// 表格邊框樣式(統(tǒng)一加粗黑色邊框)
const tableBorder = {
style: BorderStyle.SINGLE,
size: 3,
color: '000000'
};
// 文字基礎(chǔ)樣式(根據(jù)內(nèi)容重要性區(qū)分大?。?
const titleTextStyle = { bold: true, size: 45, color: '#FF00FF' }; // VOSS標(biāo)識
const subtitleTextStyle = { bold: true, size: 20 }; // 標(biāo)簽文本(如Issue No.)
const contentTextStyle = { size: 18 }; // 內(nèi)容文本(如編號、日期)
const positionTextStyle = { bold: true, size: 28, color: '#000000' }; // 職位名稱2.2 表格行與單元格實(shí)現(xiàn)
通過TableRow和TableCell組件構(gòu)建結(jié)構(gòu),重點(diǎn)處理列合并、寬高約束、對齊方式:
// 提取職位名稱(從選中的JD內(nèi)容中獲?。?
const positionName = selectedJdDescription
? selectedJdDescription.split('\n').filter(line => line.trim() !== '')[0]?.trim()
: '未獲取職位信息';
// 構(gòu)建頁眉表格
const headerTable = new Table({
width: { size: 100, type: WidthType.PERCENTAGE }, // 表格占滿頁面寬度
border: tableBorder, // 統(tǒng)一表格邊框
rows: [
// 第一行(包含VOSS、職位名稱、Issue No.、Issue Date)
new TableRow({
height: { value: 800, type: 'dxa' }, // 行高(dxa為Word內(nèi)部單位,800≈28px)
children: [
// 列1:VOSS標(biāo)識(水平+垂直居中)
new TableCell({
width: { size: 25, type: WidthType.PERCENTAGE },
borders: { top: tableBorder, bottom: tableBorder, left: tableBorder, right: tableBorder },
verticalAlign: 'center', // 垂直居中
children: [
new Paragraph({
children: [new TextRun({ text: 'VOSS', ...titleTextStyle })],
alignment: AlignmentType.CENTER // 水平居中
})
]
}),
// 列2:職位名稱(合并兩行,垂直居中)
new TableCell({
rowSpan: 2, // 合并兩行
width: { size: 35, type: WidthType.PERCENTAGE },
borders: { top: tableBorder, bottom: tableBorder, left: tableBorder, right: tableBorder },
verticalAlign: 'center',
children: [
new Paragraph({
children: [new TextRun({ text: positionName, ...positionTextStyle })],
alignment: AlignmentType.CENTER
})
]
}),
// 列3:Issue No. 與編號
new TableCell({
width: { size: 20, type: WidthType.PERCENTAGE },
borders: { top: tableBorder, bottom: tableBorder, left: tableBorder, right: tableBorder },
verticalAlign: 'center',
children: [
new Paragraph({
children: [new TextRun({ text: 'Issue No.', ...subtitleTextStyle })],
alignment: AlignmentType.CENTER,
spacing: { after: 25 } // 文字間距
}),
new Paragraph({
children: [new TextRun({ text: '001-F002', ...contentTextStyle })],
alignment: AlignmentType.CENTER
})
]
}),
// 列4:Issue Date 與日期
new TableCell({
width: { size: 20, type: WidthType.PERCENTAGE },
borders: { top: tableBorder, bottom: tableBorder, left: tableBorder, right: tableBorder },
verticalAlign: 'center',
children: [
new Paragraph({
children: [new TextRun({ text: 'Issue Date', ...subtitleTextStyle })],
alignment: AlignmentType.CENTER,
spacing: { after: 25 }
}),
new Paragraph({
children: [new TextRun({ text: '2024/9/21', ...contentTextStyle })],
alignment: AlignmentType.CENTER
})
]
})
]
}),
// 第二行(包含Job Description、Compiled By、Approved By)
new TableRow({
height: { value: 600, type: 'dxa' },
children: [
// 列1:Job Description + 職位描述
new TableCell({
width: { size: 25, type: WidthType.PERCENTAGE },
borders: { top: tableBorder, bottom: tableBorder, left: tableBorder, right: tableBorder },
verticalAlign: 'center',
children: [
new Paragraph({
children: [new TextRun({ text: 'Job Description', ...subtitleTextStyle })],
alignment: AlignmentType.CENTER,
spacing: { after: 15 }
}),
new Paragraph({
children: [new TextRun({ text: '職位描述', ...contentTextStyle })],
alignment: AlignmentType.CENTER
})
]
}),
// 列3:Compiled By(編制人,綁定頁面輸入值)
new TableCell({
width: { size: 20, type: WidthType.PERCENTAGE },
borders: { top: tableBorder, bottom: tableBorder, left: tableBorder, right: tableBorder },
verticalAlign: 'center',
children: [
new Paragraph({
children: [new TextRun({ text: 'Compiled By', ...subtitleTextStyle })],
alignment: AlignmentType.CENTER,
spacing: { after: 25 }
}),
new Paragraph({
children: [new TextRun({ text: compiledBy || '未填寫', ...contentTextStyle })],
alignment: AlignmentType.CENTER
})
]
}),
// 列4:Approved By(審批人,綁定頁面輸入值)
new TableCell({
width: { size: 20, type: WidthType.PERCENTAGE },
borders: { top: tableBorder, bottom: tableBorder, left: tableBorder, right: tableBorder },
verticalAlign: 'center',
children: [
new Paragraph({
children: [new TextRun({ text: 'Approved By', ...subtitleTextStyle })],
alignment: AlignmentType.CENTER,
spacing: { after: 25 }
}),
new Paragraph({
children: [new TextRun({ text: approvedBy || '未填寫', ...contentTextStyle })],
alignment: AlignmentType.CENTER
})
]
})
]
})
]
});3. 頁眉與文檔集成
將構(gòu)建好的表格包裝為 Word 頁眉,并配置到文檔中,確保每一頁都顯示該頁眉:
// 3.1 處理 JD 內(nèi)容,轉(zhuǎn)換為 Word 標(biāo)準(zhǔn)格式(同步放大文字大小)
const jdParagraphs = [];
const lines = selectedJdDescription.split('\n');
let firstHeadingFound = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmedLine = line.trim();
if (trimmedLine === '') {
// 空行:保留間距,優(yōu)化閱讀體驗(yàn)
jdParagraphs.push(new Paragraph({
spacing: { after: 80 }, // 空行間距(dxa單位,80≈2.8px)
}));
} else if (trimmedLine.startsWith('# ')) {
// 一級標(biāo)題:除首個標(biāo)題外,其余標(biāo)題前添加分頁符
if (firstHeadingFound) {
jdParagraphs.push(new PageBreak()); // 分頁符,標(biāo)題獨(dú)立起頁
} else {
firstHeadingFound = true; // 標(biāo)記已找到首個標(biāo)題
}
// 一級標(biāo)題樣式:加粗、放大字號、左對齊、底部間距
jdParagraphs.push(new Paragraph({
children: [new TextRun({
text: trimmedLine.slice(2), // 移除Markdown前綴 "# "
bold: true,
size: 24, // 字體大?。?4=12pt,放大后更醒目)
color: "#000000",
font: "Arial"
})],
heading: HeadingLevel.HEADING_1, // 綁定Word一級標(biāo)題樣式(支持目錄生成)
spacing: { after: 150 }, // 標(biāo)題底部間距(150≈5.3px)
alignment: AlignmentType.LEFT,
}));
} else if (trimmedLine.startsWith('- ')) {
// 列表項(xiàng):添加項(xiàng)目符號、放大字號、保留間距
jdParagraphs.push(new Paragraph({
children: [new TextRun({
text: trimmedLine.slice(2), // 移除Markdown前綴 "- "
size: 20, // 字體大?。?0=10pt,適配閱讀)
color: "#333333",
font: "Arial"
})],
bullet: { level: 0 }, // 一級項(xiàng)目符號(實(shí)心圓點(diǎn))
spacing: { after: 80 }, // 列表項(xiàng)底部間距
alignment: AlignmentType.LEFT,
}));
} else {
// 普通文本:放大字號、左對齊、統(tǒng)一間距
jdParagraphs.push(new Paragraph({
children: [new TextRun({
text: trimmedLine,
size: 20, // 字體大?。?0=10pt,比默認(rèn)放大更易讀)
color: "#333333",
font: "Arial"
})],
spacing: { after: 80 }, // 文本行底部間距
alignment: AlignmentType.LEFT,
}));
}
}
// 3.2 包裝頁眉(default 表示所有頁面共用該頁眉)
const header = new Header({
children: [headerTable], // 傳入之前構(gòu)建的4列2行頁眉表格
margin: { top: 50, bottom: 50 } // 頁眉內(nèi)邊距(優(yōu)化與正文間距)
});
// 3.3 創(chuàng)建Word文檔,整合頁眉與格式化正文
const doc = new Document({
sections: [
{
// 頁面配置:適配頁眉高度,設(shè)置合理邊距
properties: {
page: {
margin: {
top: 1800, // 頂部邊距(1800≈63px,預(yù)留頁眉空間)
bottom: 900, // 底部邊距(900≈31.5px)
left: 1260, // 左側(cè)邊距(1260≈44px)
right: 1260, // 右側(cè)邊距(1260≈44px)
},
size: {
width: 11906, // A4紙寬度(默認(rèn)值,適配標(biāo)準(zhǔn)打印)
height: 16838, // A4紙高度(默認(rèn)值)
}
}
},
headers: { default: header }, // 綁定全局頁眉(所有頁面顯示)
children: [
// 正文內(nèi)容:先添加分頁符(避免頁眉與正文重疊),再導(dǎo)入格式化后的段落
new PageBreak(),
...jdParagraphs // 展開處理后的JD正文段落
]
}
]
});
// 3.4 生成Blob對象并觸發(fā)下載
const blob = await Packer.toBlob(doc); // 將文檔打包為Blob
const fileName = `${positionName}_JD_${formattedIssueDate}.docx`; // 文件名:職位名稱_日期.docx
saveAs(blob, fileName); // 調(diào)用file-saver下載文件
message.success('JD文檔導(dǎo)出成功!');五、handleExportJd 完整邏輯
const handleExportJd = async () => {
try {
// 動態(tài)導(dǎo)入 docx 庫(適配 9.5.1 版本)
const docx = await import('docx');
const {
Document,
Packer,
TextRun,
Paragraph,
Table,
TableRow,
TableCell,
WidthType,
BorderStyle,
AlignmentType,
HeadingLevel,
Header,
SectionType,
PageBreak
} = docx;
// 從 selectedJdDescription 中提取第一個非空行作為職位名稱
const getPositionName = () => {
if (!selectedJdDescription) return '未獲取職位信息';
// 按換行符分割,過濾空行和純空格行
const lines = selectedJdDescription.split('\n').filter(line => line.trim() !== '');
// 返回第一個非空行的內(nèi)容(去除前后空格)
return lines.length > 0 ? lines[0].trim() : '未獲取職位信息';
};
const positionName = getPositionName();
// 創(chuàng)建頁眉表格
const headerTable = new Table({
rows: [
new TableRow({
height: { value: 800, type: 'dxa' }, // 增加行高(dxa 為 Word 內(nèi)部單位)
children: [
// 第一列第一行:僅顯示 VOSS
new TableCell({
children: [
new Paragraph({
children: [new TextRun({ text: 'VOSS', bold: true, size: 45, color: '#FF00FF' })],
alignment: AlignmentType.CENTER, // 水平居中
spacing: { after: 0 },
}),
],
width: { size: 25, type: WidthType.PERCENTAGE },
borders: {
top: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
bottom: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
left: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
right: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
},
verticalAlign: 'center',
}),
// 第二列:合并行顯示提取的職位名稱
new TableCell({
rowSpan: 2,
children: [
new Paragraph({
children: [new TextRun({
text: positionName,
bold: true,
size: 28,
color: '#000000'
})],
alignment: AlignmentType.CENTER,
spacing: { after: 40 },
})
],
width: { size: 35, type: WidthType.PERCENTAGE },
borders: {
top: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
bottom: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
left: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
right: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
},
verticalAlign: 'center',
}),
// 第三列第一行:Issue No.
new TableCell({
children: [
new Paragraph({
children: [new TextRun({ text: 'Issue No.', bold: true, size: 20 })],
alignment: AlignmentType.CENTER,
spacing: { after: 25 },
}),
new Paragraph({
children: [new TextRun({ text: '001-F002', size: 18 })],
alignment: AlignmentType.CENTER,
}),
],
width: { size: 20, type: WidthType.PERCENTAGE },
borders: {
top: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
bottom: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
left: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
right: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
},
verticalAlign: 'center',
}),
// 第四列第一行:Issue Date
new TableCell({
children: [
new Paragraph({
children: [new TextRun({ text: 'Issue Date', bold: true, size: 20 })],
alignment: AlignmentType.CENTER,
spacing: { after: 25 },
}),
new Paragraph({
children: [new TextRun({ text: '2024/9/21', size: 18 })],
alignment: AlignmentType.CENTER,
}),
],
width: { size: 20, type: WidthType.PERCENTAGE },
borders: {
top: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
bottom: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
left: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
right: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
},
verticalAlign: 'center',
}),
],
}),
new TableRow({
height: { value: 600, type: 'dxa' },
children: [
// 第一列第二行:顯示 Job Description 和 職位描述(保持居中)
new TableCell({
children: [
new Paragraph({
children: [new TextRun({ text: 'Job Description', bold: true, size: 20 })],
alignment: AlignmentType.CENTER,
spacing: { after: 15 },
}),
new Paragraph({
children: [new TextRun({ text: '職位描述', size: 18 })],
alignment: AlignmentType.CENTER,
}),
],
width: { size: 25, type: WidthType.PERCENTAGE },
borders: {
top: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
bottom: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
left: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
right: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
},
verticalAlign: 'center',
}),
// 第三列第二行:Compiled By
new TableCell({
children: [
new Paragraph({
children: [new TextRun({ text: 'Compiled By', bold: true, size: 20 })],
alignment: AlignmentType.CENTER,
spacing: { after: 25 },
}),
new Paragraph({
children: [new TextRun({ text: compiledBy || '未填寫', size: 18 })],
alignment: AlignmentType.CENTER,
}),
],
width: { size: 20, type: WidthType.PERCENTAGE },
borders: {
top: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
bottom: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
left: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
right: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
},
verticalAlign: 'center',
}),
// 第四列第二行:Approved By
new TableCell({
children: [
new Paragraph({
children: [new TextRun({ text: 'Approved By', bold: true, size: 20 })],
alignment: AlignmentType.CENTER,
spacing: { after: 25 },
}),
new Paragraph({
children: [new TextRun({ text: approvedBy || '未填寫', size: 18 })],
alignment: AlignmentType.CENTER,
}),
],
width: { size: 20, type: WidthType.PERCENTAGE },
borders: {
top: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
bottom: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
left: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
right: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
},
verticalAlign: 'center',
}),
],
}),
],
width: { size: 100, type: WidthType.PERCENTAGE },
border: { style: BorderStyle.SINGLE, size: 3, color: '000000' },
});
// 將表格包裝成頁眉對象
const header = new Header({
children: [headerTable],
});
// 處理 JD 內(nèi)容,轉(zhuǎn)換為 Word 段落(同步放大文字大小)
const jdParagraphs = [];
const lines = selectedJdDescription.split('\n');
let firstHeadingFound = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmedLine = line.trim();
if (trimmedLine === '') {
// 空行(調(diào)整間距)
jdParagraphs.push(new Paragraph({
spacing: { after: 80 },
}));
} else if (trimmedLine.startsWith('# ')) {
// 一級標(biāo)題,前添加分頁符(除了第一個標(biāo)題)
if (firstHeadingFound) {
jdParagraphs.push(new PageBreak());
} else {
firstHeadingFound = true;
}
jdParagraphs.push(new Paragraph({
children: [new TextRun({ text: trimmedLine.slice(2), bold: true, size: 24 })],
heading: HeadingLevel.HEADING_1,
spacing: { after: 150 },
alignment: AlignmentType.LEFT,
}));
} else if (trimmedLine.startsWith('- ')) {
// 列表項(xiàng)
jdParagraphs.push(new Paragraph({
children: [new TextRun({ text: trimmedLine.slice(2), size: 20 })],
bullet: { level: 0 },
spacing: { after: 80 },
alignment: AlignmentType.LEFT,
}));
} else {
// 普通文本
jdParagraphs.push(new Paragraph({
children: [new TextRun({ text: trimmedLine, size: 20 })],
spacing: { after: 80 },
alignment: AlignmentType.LEFT,
}));
}
}
// 創(chuàng)建文檔(調(diào)整頁面邊距以適應(yīng)放大的表格)
const doc = new Document({
sections: [
{
properties: {
page: {
margin: {
top: 1800, // 增加頂部邊距(適應(yīng)放大后的頁眉)
bottom: 900,
left: 1260,
right: 1260,
},
},
},
// 設(shè)置頁眉(每一頁顯示)
headers: {
default: header,
},
children: [
new PageBreak(), // 標(biāo)題后分頁
...jdParagraphs,
],
},
],
});
// 導(dǎo)出文檔
const blob = await Packer.toBlob(doc);
const fileName = positionName !== '未獲取職位信息' ? `${positionName}_JD.docx` : 'JD文檔.docx';
saveAs(blob, fileName);
antdApp.message.success('導(dǎo)出 Word 成功');
} catch (error) {
console.error('導(dǎo)出 Word 失?。?, error);
antdApp.message.error('導(dǎo)出 Word 失敗,請檢查文檔內(nèi)容格式');
}
};如果遇到發(fā)送給其他人(windows、mac)打不開或者不兼容
const handleExportJd = async () => {
try {
// 動態(tài)導(dǎo)入 docx 庫
const docx = await import('docx');
const {
Document,
Packer,
TextRun,
Paragraph,
Table,
TableRow,
TableCell,
WidthType,
BorderStyle,
AlignmentType,
HeadingLevel,
Header,
PageBreak,
} = docx;
// 從 selectedJdDescription 中提取第一個非空行作為職位名稱
const getPositionName = () => {
if (!selectedJdDescription) return '未獲取職位信息';
const lines = selectedJdDescription.split('\n').filter(line => line.trim() !== '');
return lines.length > 0 ? lines[0].trim() : '未獲取職位信息';
};
const positionName = getPositionName();
// 創(chuàng)建頁眉表格
const headerTable = new Table({
rows: [
new TableRow({
children: [
// 第一列第一行:僅顯示 VOSS
new TableCell({
children: [
new Paragraph({
children: [new TextRun({ text: 'VOSS', bold: true, size: 35, color: '#FF00FF' })],
alignment: AlignmentType.CENTER,
}),
],
width: { size: 25, type: WidthType.PERCENTAGE },
borders: {
top: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
bottom: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
left: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
right: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
},
}),
// 第二列:合并行顯示提取的職位名稱
new TableCell({
rowSpan: 2,
children: [
new Paragraph({
children: [new TextRun({
text: positionName,
bold: true,
size: 22,
color: '#000000'
})],
alignment: AlignmentType.CENTER,
})
],
width: { size: 35, type: WidthType.PERCENTAGE },
borders: {
top: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
bottom: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
left: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
right: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
},
verticalAlign: AlignmentType.CENTER, // 設(shè)置垂直居中對齊
}),
// 第三列第一行:Issue No.
new TableCell({
children: [
new Paragraph({
children: [new TextRun({ text: 'Issue No.', bold: true, size: 18 })],
alignment: AlignmentType.CENTER,
}),
new Paragraph({
children: [new TextRun({ text: issueNo || '未填寫', size: 16 })],
alignment: AlignmentType.CENTER,
}),
],
width: { size: 20, type: WidthType.PERCENTAGE },
borders: {
top: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
bottom: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
left: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
right: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
},
}),
// 第四列第一行:Issue Date
new TableCell({
children: [
new Paragraph({
children: [new TextRun({ text: 'Issue Date', bold: true, size: 18 })],
alignment: AlignmentType.CENTER,
}),
new Paragraph({
children: [new TextRun({ text: issueDate || '未填寫', size: 16 })],
alignment: AlignmentType.CENTER,
}),
],
width: { size: 20, type: WidthType.PERCENTAGE },
borders: {
top: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
bottom: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
left: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
right: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
},
}),
],
}),
new TableRow({
children: [
// 第一列第二行:顯示 Job Description 和 職位描述
new TableCell({
children: [
new Paragraph({
children: [new TextRun({ text: 'Job Description', bold: true, size: 18 })],
alignment: AlignmentType.CENTER,
}),
new Paragraph({
children: [new TextRun({ text: '職位描述', size: 16 })],
alignment: AlignmentType.CENTER,
}),
],
width: { size: 25, type: WidthType.PERCENTAGE },
borders: {
top: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
bottom: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
left: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
right: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
},
}),
// 第三列第二行:Compiled By
new TableCell({
children: [
new Paragraph({
children: [new TextRun({ text: 'Compiled By', bold: true, size: 18 })],
alignment: AlignmentType.CENTER,
}),
new Paragraph({
children: [new TextRun({ text: compiledBy || '未填寫', size: 16 })],
alignment: AlignmentType.CENTER,
}),
],
width: { size: 20, type: WidthType.PERCENTAGE },
borders: {
top: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
bottom: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
left: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
right: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
},
}),
// 第四列第二行:Approved By
new TableCell({
children: [
new Paragraph({
children: [new TextRun({ text: 'Approved By', bold: true, size: 18 })],
alignment: AlignmentType.CENTER,
}),
new Paragraph({
children: [new TextRun({ text: approvedBy || '未填寫', size: 16 })],
alignment: AlignmentType.CENTER,
}),
],
width: { size: 20, type: WidthType.PERCENTAGE },
borders: {
top: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
bottom: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
left: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
right: { style: BorderStyle.SINGLE, size: 2, color: '000000' },
},
}),
],
}),
],
width: { size: 100, type: WidthType.PERCENTAGE },
});
// 將表格包裝成頁眉對象,簡化配置以確保兼容性
const header = new Header({
children: [headerTable],
// 設(shè)置頁眉的邊距和定位
properties: {
headerPosition: {
value: 0, // 距離頁面頂部的距離
},
},
});
// 處理 JD 內(nèi)容,轉(zhuǎn)換為 Word 段落
const jdParagraphs = [];
const lines = selectedJdDescription.split('\n');
let firstHeadingFound = false;
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine === '') {
// 空行
jdParagraphs.push(new Paragraph({ spacing: { after: 50 } }));
} else if (trimmedLine.startsWith('# ')) {
// 一級標(biāo)題
if (firstHeadingFound) {
jdParagraphs.push(new PageBreak());
} else {
firstHeadingFound = true;
}
jdParagraphs.push(new Paragraph({
children: [new TextRun({ text: trimmedLine.slice(2), bold: true, size: 22 })],
heading: HeadingLevel.HEADING_1,
spacing: { after: 100 },
}));
} else if (trimmedLine.startsWith('- ')) {
// 列表項(xiàng)
jdParagraphs.push(new Paragraph({
children: [new TextRun({ text: trimmedLine.slice(2), size: 18 })],
bullet: { level: 0 },
spacing: { after: 50 },
}));
} else {
// 普通文本
jdParagraphs.push(new Paragraph({
children: [new TextRun({ text: trimmedLine, size: 18 })],
spacing: { after: 50 },
}));
}
}
// 創(chuàng)建文檔
const doc = new Document({
sections: [
{
properties: {
page: {
margin: {
top: 1800, // 增加頂部邊距以確保頁眉能正確顯示
bottom: 800,
left: 1000,
right: 1000,
},
},
// 確保所有頁面都使用相同的頁眉配置
headerFooter: {
differentFirstPage: false,
differentOddEven: false,
},
},
// 為所有頁面類型都設(shè)置相同的頁眉
headers: {
default: header,
first: header,
even: header,
odd: header,
},
children: [...jdParagraphs],
},
],
});
// 導(dǎo)出文檔 - 使用更可靠的方式處理 blob
const blob = await Packer.toBlob(doc);
const fileName = positionName !== '未獲取職位信息' ? `${positionName}_JD.docx` : 'JD文檔.docx';
try {
// 確保blob類型正確
const docxBlob = new Blob([blob], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
// 使用標(biāo)準(zhǔn)的下載方法,避免依賴外部庫
const url = window.URL.createObjectURL(docxBlob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
// 觸發(fā)下載
if (document.createEvent) {
const event = document.createEvent('MouseEvents');
event.initEvent('click', true, true);
a.dispatchEvent(event);
} else {
a.click();
}
// 清理
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
antdApp.message.success('導(dǎo)出 Word 成功');
} catch (downloadError) {
console.error('下載文件失?。?, downloadError);
// 降級處理:嘗試直接打開blob
const url = window.URL.createObjectURL(blob);
window.open(url, '_blank');
antdApp.message.info('導(dǎo)出成功,文件已在新窗口打開');
}
} catch (error) {
console.error('導(dǎo)出 Word 失敗:', error);
// 提供更詳細(xì)的錯誤信息
if (error instanceof Error) {
console.error('Error message:', error.message);
console.error('Error stack:', error.stack);
}
antdApp.message.error('導(dǎo)出 Word 失敗,請檢查文檔內(nèi)容格式');
}
};僅供參考!??!
六、關(guān)鍵技術(shù)點(diǎn)總結(jié)
- 列合并實(shí)現(xiàn):通過
TableCell的rowSpan屬性實(shí)現(xiàn)跨行合并,適用于需要縱向占用多行的內(nèi)容(如職位名稱)。 - 寬高控制:列寬使用
WidthType.PERCENTAGE(百分比)確保適配不同頁面尺寸,行高使用dxa單位(Word 原生單位)確保精度。 - 對齊方式:結(jié)合
verticalAlign: 'center'(垂直居中)和AlignmentType.CENTER(水平居中),實(shí)現(xiàn)表格內(nèi)容的完全居中。 - 樣式復(fù)用:提取公共樣式(邊框、文字大?。?,降低代碼冗余,便于后續(xù)維護(hù)。
七、注意事項(xiàng)
docx庫的單位差異:dxa是 Word 內(nèi)部長度單位(1pt≈20dxa),設(shè)置行高時(shí)需注意換算,避免頁眉過高或過低。- 邊框樣式一致性:需為每個單元格單獨(dú)設(shè)置邊框(或通過表格全局邊框配置),避免出現(xiàn)邊框缺失。
- 動態(tài)導(dǎo)入兼容性:確保
docx版本與導(dǎo)入方式匹配(v9.5.1 支持 ES 模塊動態(tài)導(dǎo)入),避免版本沖突。
八、最終呈現(xiàn)效果
下載到本地之后打開即可:

通過以上方案,可實(shí)現(xiàn)結(jié)構(gòu)固定、樣式統(tǒng)一、數(shù)據(jù)動態(tài)的自定義頁眉表格,滿足 Word 導(dǎo)出場景中的復(fù)雜頁眉需求,同時(shí)保證導(dǎo)出文檔的專業(yè)性和規(guī)范性。
注意?: 本文主要是針對如何實(shí)現(xiàn) word 文檔頁眉表格形式的設(shè)置,其余邏輯以及業(yè)務(wù)需自行修改?。?!
到此這篇關(guān)于前端Word導(dǎo)出自定義頁眉表格實(shí)現(xiàn)方案的文章就介紹到這了,更多相關(guān)前端Word導(dǎo)出自定義頁眉表格內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Vue前端實(shí)現(xiàn)導(dǎo)出頁面為word的兩種方法
- vue導(dǎo)出word純前端的實(shí)現(xiàn)方式
- 前端導(dǎo)出word文件的多種方式以及導(dǎo)出excel文件
- vue前端實(shí)現(xiàn)導(dǎo)出頁面為word的兩種方法代碼
- 前端將html導(dǎo)出為word文件的功能實(shí)現(xiàn)
- vue純前端實(shí)現(xiàn)將頁面導(dǎo)出為pdf和word文件
- 前端基于mammoth.js實(shí)現(xiàn)Word文檔在線編輯與導(dǎo)出
- 前端docx庫實(shí)現(xiàn)將html頁面導(dǎo)出word的詳細(xì)過程
相關(guān)文章
最簡單的JS實(shí)現(xiàn)json轉(zhuǎn)csv的方法
這篇文章主要介紹了最簡單的JS實(shí)現(xiàn)json轉(zhuǎn)csv的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-01-01
利用svg實(shí)現(xiàn)帶加載進(jìn)度的loading
svg是基于XML,由World?Wide?Web?Consortium?(W3C)聯(lián)盟開發(fā)的一種開放標(biāo)準(zhǔn)的矢量圖形語言,可讓你設(shè)計(jì)激動人心的、高分辨率的Web圖形頁面。本文將使用svg實(shí)現(xiàn)一個帶加載進(jìn)度的loading,需要的可以參考一下2022-11-11
實(shí)現(xiàn)點(diǎn)擊列表彈出列表索引的兩種方式
使用利用事件冒泡委托給列表的父節(jié)點(diǎn)去處理的方式第二種方式就是使用閉包了,感興趣的你可以參考下本文,或許對你學(xué)習(xí)js有所幫助2013-03-03
javascript正則表達(dá)式模糊匹配IP地址功能示例
這篇文章主要介紹了javascript正則表達(dá)式模糊匹配IP地址功能,結(jié)合簡單實(shí)例形式演示了JS模糊匹配IP地址的實(shí)現(xiàn)方法,涉及針對數(shù)字及字符串的相關(guān)正則判定與匹配操作技巧,需要的朋友可以參考下2017-01-01
用Javascript實(shí)現(xiàn)Windows任務(wù)管理器的代碼
在Windows系統(tǒng)上,自從98系統(tǒng)以來就提供了腳本宿主(Windows Scripting Host 簡稱WSH)的功能,WSH可以加載并運(yùn)行JS和VBS腳本,并支持調(diào)用系統(tǒng)的COM組件,在COM組件的支持下腳本可以輕松實(shí)現(xiàn)非常強(qiáng)大的功能2012-03-03
bootstrap datetimepicker2.3.11時(shí)間插件使用
這篇文章主要為大家詳細(xì)介紹了bootstrap datetimepicker2.3.11時(shí)間插件使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11
bootstrap table實(shí)現(xiàn)單擊單元格可編輯功能
這篇文章主要為大家詳細(xì)介紹了bootstrap table實(shí)現(xiàn)單擊單元格可編輯功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
下載站控制介紹字?jǐn)?shù)顯示的腳本 顯示全部 隱藏介紹等功能
有一些下載網(wǎng)站為了整體的統(tǒng)一,盡量的控制軟件介紹的字?jǐn)?shù)顯示,要不因?yàn)樽謹(jǐn)?shù)介紹太多導(dǎo)致用戶看到下載鏈接比較靠后等原因而一個小功能的實(shí)現(xiàn)。2009-09-09

