需求
最近有项目需要用到生成word文档,平时经常用的都是通过模板生成,里面变量使用占位符替换,好处是快捷、方便、简单、不需要通过代码调word样式,确定是很多库不支持图片绘制(很多都是付费功能),找一圈,发现一个很有意思的库,正好也满足我们的需求,特此分享一下
依赖
// https://docx.js.org/#/
npm i docx
// https://www.npmjs.com/package/download
npm i download
说明,因为docx绘图只支持文件流,所以要把网络文件下载到本地转成buffer
代码
话不多说,上代码
import * as fs from "fs"
import { Document, Packer, Paragraph, TextRun, ImageRun, HeadingLevel, AlignmentType, convertInchesToTwip, Table, TableRow, TableCell, WidthType, VerticalAlign, BorderStyle } from "docx"
const download = require('download')
// 性别
enum Gender {
Male = 'male',
Female = 'female'
}
// 选手
type PlayerSchema = {
name: string
gender: string
idCard?: string
birthday?: string
weight?: string
remark?: string
avatar?: string
localAvatar?: string
level: string
}
type GroupSchema = {
// gender: Gender
institution: string
leader: string
phone: string
coach: string
doctor: string
players: PlayerSchema[]
}
// 所有数据
interface DataSchema {
[key: string]: GroupSchema
}
// 表格无边框
const noBoder = {
top: {
style: BorderStyle.NIL,
size: 0,
color: 'FFFFFF'
},
bottom: {
style: BorderStyle.NIL,
size: 0,
color: 'FFFFFF'
},
left: {
style: BorderStyle.NIL,
size: 0,
color: 'FFFFFF'
},
right: {
style: BorderStyle.NIL,
size: 0,
color: 'FFFFFF'
}
}
// 删除下载的照片及文件夹
function delStaticFile(groupNames: string[]) {
for (let groupName of groupNames) {
if (fs.existsSync(groupName)) {
const files = fs.readdirSync(groupName)
files.map((file: string) => {
let curPath = groupName + "/" + file
// 删除选手招聘
fs.unlinkSync(curPath)
})
fs.rmdirSync(groupName)
}
}
}
// 生成word
async function generate (data: DataSchema) {
const groupNames = Object.keys(data)
// 比较粗糙的控制单元格长度逻辑
const longHeaders = ['身份证号', '备注']
// 下载远程资源到本地
for (let groupName of groupNames) {
if (!fs.existsSync(groupName)) {
fs.mkdirSync(groupName)
}
const players = data[groupName].players
for (let player of players) {
if (player.avatar) {
const avatarArr = player.avatar.split('/')
const fileName = `${groupName}/${avatarArr[avatarArr.length - 1]}`
if (!fs.existsSync(fileName)) {
await download(player.avatar, groupName)
}
// 下载后的本地的资源路径
player.localAvatar = fileName
}
}
}
// 需要多个文件合一
const sections = groupNames.map(groupName => {
const info = data[groupName]
const { institution, leader, phone, coach, doctor, players } = info
// 标头内容
// let headers = ['序号', '照片', '姓名', '性别', '出生年月', '体重', '级别', '备注']
let headers = ['序号', '照片', '姓名', '性别', '身份证号', '级别', '备注']
// 表格数据
let tableData: any[][] = []
tableData.push(headers)
// 填充选手信息
let index = 1
for (let player of players) {
tableData.push([
index.toString(),
player.localAvatar || '',
player.name,
player.gender === Gender.Male ? '男' : '女',
player.idCard,
// player.birthday,
// player.weight,
player.level,
player.remark,
])
index++
}
// 表格渲染
const tableRows = tableData.map(colums => {
return new TableRow({
children: colums.map(cell => {
return new TableCell({
verticalAlign: VerticalAlign.CENTER,
width: {
// 设置宽度 dxa长度单位 https://stackoverflow.com/questions/14360183/default-wordml-unit-measurement-pixel-or-point-or-inches
size: longHeaders.some(j => cell === j) ? 3000 : 800,
type: WidthType.DXA,
},
children: cell && colums.findIndex(i => i === cell) === 1 && cell !== '照片' ?
[new Paragraph({
alignment: AlignmentType.CENTER,
children: [
new ImageRun({
// 将图片转化为buffer
data: fs.readFileSync(cell),
transformation: {
width: 100,
height: 129,
},
})
]
})]:
[new Paragraph({
alignment: AlignmentType.CENTER,
children:[
new TextRun(cell || '')
]
})]
})
})
})
})
// 渲染报名表格
const table = new Table({
alignment: AlignmentType.CENTER,
rows: tableRows
})
return {
properties: {},
children: [
// new Paragraph({
// style: "wellSpaced",
// children: [
// new TextRun({
// text: '附件 4',
// color: '999999',
// })
// ],
// }),
// 表头信息
new Paragraph({
spacing: {
before: 400,
after: 400
},
style: "Title",
text: `自 由 搏 击 比 赛 报 名 表(${groupName === Gender.Male ? '男子' : '女子'})`,
heading: HeadingLevel.TITLE,
alignment: AlignmentType.CENTER
}),
// 队伍信息
new Table({
style: "wellSpaced",
alignment: AlignmentType.CENTER,
borders: noBoder,
rows: [
new TableRow({
children: [
new TableCell({
width: {
size: 600,
type: WidthType.DXA,
},
borders: noBoder,
children: [
new Paragraph(`单位: `),
],
}),
new TableCell({
width: {
size: 1800,
type: WidthType.DXA,
},
borders: noBoder,
children: [
new Paragraph(`${institution}`)
],
}),
new TableCell({
width: {
size: 700,
type: WidthType.DXA,
},
borders: noBoder,
children: [
new Paragraph(` 领队: `),
],
}),
new TableCell({
width: {
size: 1200,
type: WidthType.DXA,
},
borders: noBoder,
children: [
new Paragraph(`${leader}`)
],
}),
new TableCell({
width: {
size: 1100,
type: WidthType.DXA,
},
borders: noBoder,
children: [
new Paragraph(` 联系电话: `),
],
}),
new TableCell({
width: {
size: 1400,
type: WidthType.DXA,
},
borders: noBoder,
children: [
new Paragraph(`${phone}`)
],
}),
new TableCell({
width: {
size: 700,
type: WidthType.DXA,
},
borders: noBoder,
children: [
new Paragraph(` 教练: `),
],
}),
new TableCell({
width: {
size: 1300,
type: WidthType.DXA,
},
borders: noBoder,
children: [
new Paragraph(`${coach}`)
],
}),
new TableCell({
width: {
size: 700,
type: WidthType.DXA,
},
borders: noBoder,
children: [
new Paragraph(` 队医: `),
],
}),
new TableCell({
width: {
size: 1300,
type: WidthType.DXA,
},
borders: noBoder,
children: [
new Paragraph(`${doctor}`)
],
}),
],
}),
]
}),
// 用于段落距离(table无法设置spacing属性)
new Paragraph({
spacing: {
// 通过调整before值来调整段落渐进
before: 400,
},
text: ``,
}),
// 选手信息
table,
// 印章和时间
new Paragraph({
style: "wellSpaced",
children: [
new TextRun({
text: '\t\t\t\t报名单位章:\t\t\t\t\t\t',
}),
new TextRun({
text: '年\t\t'
}),
new TextRun({
text: '月\t\t'
}),
new TextRun({
text: '日'
})
]
})
]
}
})
// 创建整个文档
const doc = new Document({
styles: {
paragraphStyles: [
{
id: "Title",
name: "title",
basedOn: "Normal",
next: "Normal",
quickFormat: true,
run: {
size: 30,
bold: true,
color: "000000"
}
},
{
id: "wellSpaced",
name: "Well Spaced",
basedOn: "Normal",
quickFormat: true,
paragraph: {
indent: {
left: convertInchesToTwip(0.5),
},
spacing: {
before: 400,
},
},
},
],
},
sections
})
// 生成word文档
Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("enrolls.docx", buffer)
})
// 删除下载的选手照片
delStaticFile(groupNames)
}
const group: GroupSchema = {
institution: '江苏省南京市舜禹集团总部',
leader: '王猛(男)',
phone: '18861856665',
coach: '刘国梁(男)',
doctor: '杨永信(女)',
players: [
{
name: '莱昂纳多迪卡普里奥',
gender: Gender.Male,
idCard: '320888199001019878',
birthday: '1999-01-02',
weight: '60kg',
avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/13.png',
remark: '',
level: '60kg'
},
{
name: '张三',
gender: Gender.Male,
idCard: '320888199001019878',
birthday: '1999-01-02',
weight: '60kg',
avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/7.png',
remark: '',
level: '60kg'
},
{
name: '张三',
gender: Gender.Male,
idCard: '320888199001019878',
birthday: '1999-01-02',
weight: '60kg',
avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/14.png',
remark: '',
level: '60kg'
},
{
name: '张三',
gender: Gender.Male,
idCard: '320888199001019878',
birthday: '1999-01-02',
weight: '60kg',
avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/14.png',
remark: '',
level: '60kg'
},
{
name: '张三',
gender: Gender.Male,
idCard: '320888199001019878',
birthday: '1999-01-02',
weight: '60kg',
avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/14.png',
remark: '',
level: '60kg'
},
{
name: '张三',
gender: Gender.Male,
idCard: '320888199001019878',
birthday: '1999-01-02',
weight: '60kg',
avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/14.png',
remark: '',
level: '60kg'
},
{
name: '张三',
gender: Gender.Male,
idCard: '320888199001019878',
birthday: '1999-01-02',
weight: '60kg',
avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/14.png',
remark: '',
level: '60kg'
},
{
name: '张三',
gender: Gender.Male,
idCard: '320888199001019878',
birthday: '1999-01-02',
weight: '60kg',
avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/14.png',
remark: '',
level: '60kg'
},
{
name: '张三',
gender: Gender.Male,
birthday: '1999-01-02',
weight: '60kg',
avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/14.png',
idCard: '320888199001019878',
remark: '',
level: '60kg'
},
{
name: '张三',
gender: Gender.Male,
idCard: '320888199001019878',
birthday: '1999-01-02',
weight: '60kg',
avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/14.png',
remark: '',
level: '60kg'
},
{
name: '张三',
gender: Gender.Male,
idCard: '320888199001019878',
birthday: '1999-01-02',
weight: '60kg',
avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/14.png',
remark: '',
level: '60kg'
},
{
name: '张三',
gender: Gender.Male,
idCard: '320888199001019878',
birthday: '1999-01-02',
weight: '60kg',
avatar: 'https://multi-xm.oss-cn-hangzhou.aliyuncs.com/atms/14.png',
remark: '',
level: '60kg'
}
]
}
const data: DataSchema = {
[Gender.Male]: group,
[Gender.Female]: group,
}
generate(data)