feat: printTemplate (#1338)

Co-authored-by: root <root@huan.daoyoucloud.com>
Co-authored-by: sealday <zhanglin@daoyoucloud.com>
Reviewed-on: daoyoucloud/tachybase#1338
Reviewed-by: sealday <zhanglin@daoyoucloud.com>
Co-authored-by: yoona <1486343814@qq.com>
Co-committed-by: yoona <1486343814@qq.com>
This commit is contained in:
yoona 2024-07-17 22:56:41 +08:00 committed by sealday
parent 6a151a136c
commit 49a8e2e534
15 changed files with 266 additions and 11 deletions

View File

@ -0,0 +1,2 @@
/node_modules
/src

View File

@ -0,0 +1 @@
# @tachybase/plugin-print-template

View File

@ -0,0 +1,2 @@
export * from './dist/client';
export { default } from './dist/client';

View File

@ -0,0 +1 @@
module.exports = require('./dist/client/index.js');

View File

@ -0,0 +1,15 @@
{
"name": "@tachybase/plugin-print-template",
"version": "0.21.73",
"main": "dist/server/index.js",
"dependencies": {
"docxtemplater": "^3.48.0",
"pizzip": "^3.1.7",
"xml2js": "^0.6.2"
},
"peerDependencies": {
"@tachybase/client": "workspace:*",
"@tachybase/server": "workspace:*",
"@tachybase/test": "workspace:*"
}
}

View File

@ -0,0 +1,2 @@
export * from './dist/server';
export { default } from './dist/server';

View File

@ -0,0 +1 @@
module.exports = require('./dist/server/index.js');

View File

@ -0,0 +1,21 @@
import { Plugin } from '@tachybase/client';
export class PluginPrintTemplateClient extends Plugin {
async afterAdd() {
// await this.app.pm.add()
}
async beforeLoad() {}
// You can get and modify the app instance here
async load() {
console.log(this.app);
// this.app.addComponents({})
// this.app.addScopes({})
// this.app.addProvider()
// this.app.addProviders()
// this.app.router.add()
}
}
export default PluginPrintTemplateClient;

View File

@ -0,0 +1,2 @@
export * from './server';
export { default } from './server';

View File

@ -0,0 +1,93 @@
import fs from 'fs';
import path from 'path';
import { PassThrough, Readable } from 'stream';
import { Context } from '@tachybase/actions';
import { Model } from '@tachybase/database';
import Docxtemplater from 'docxtemplater';
import PizZip from 'pizzip';
import { getTemplateParams } from './template-params'; // 导入 getTemplateParams
export const generate = async (ctx: Context) => {
const id = ctx.action.params.id;
const repo = ctx.db.getRepository('templateManage');
const template: Model = await repo.findOne({
filter: {
id,
},
appends: ['template'],
});
console.log(template.get());
const rawTemplate = template.get();
const templatePath = path.join(process.env.PWD, rawTemplate.template[0].get().url);
const data = rawTemplate.testData;
const outputPath = path.join(__dirname, './output.docx');
try {
// 获取模板中的占位符
const tags = (await getTemplateParams(templatePath)) as any;
console.log('Template tags:', tags);
// 验证数据是否包含所有占位符
const missingTags = tags.filter((tag) => !data.prototype.hasOwnProperty.call(tag.replace(/[{}]/g, '')));
if (missingTags.length > 0) {
// throw new Error(`Missing data for tags: ${missingTags.join(',')}`);
console.log('Missing data for tags');
}
// 生成定制的 docx 文件
const buffer = generateDocxFromTemplate(templatePath, data);
// 创建一个可读流
const readableStream = new Readable({
read(size) {
// 将Buffer数据push到可读流中
this.push(buffer);
// 标记流的末尾
this.push(null);
},
});
ctx.withoutDataWrapping = true;
// 设置响应类型为二进制流
ctx.set('Content-Type', 'application/octet-stream');
// 将可读流设置为响应体
ctx.body = readableStream;
// // 确保生成的 docx 文件存在
// if (!fs.existsSync(outputPath)) {
// throw new Error('Generated DOCX file not found');
// }
// ctx.body = 'Word and PDF files generated';
} catch (error) {
console.error('Error:', error);
ctx.status = 500;
ctx.body = 'Error generating files: ' + error.message;
}
};
function generateDocxFromTemplate(templatePath, data): Buffer {
try {
const content = fs.readFileSync(templatePath, 'binary');
const zip = new PizZip(content);
const doc = new Docxtemplater(zip);
doc.setData(data);
try {
doc.render();
} catch (error) {
console.error('Error rendering template:', error);
throw error;
}
return doc.getZip().generate({ type: 'nodebuffer' });
} catch (error) {
console.error('Error generating DOCX from template:', error);
throw error; // Ensure errors are propagated correctly
}
}

View File

@ -0,0 +1,54 @@
import fs from 'fs';
import PizZip from 'pizzip';
import { parseStringPromise } from 'xml2js';
export async function getTemplateParams(templatePath) {
try {
const content = fs.readFileSync(templatePath, 'binary');
const zip = new PizZip(content);
const getXmlContent = (fileName) => {
const file = zip.file(fileName);
return file ? file.asText() : '';
};
const docXml = getXmlContent('word/document.xml');
const parseXml = async (xml) => {
try {
const result = await parseStringPromise(xml, { explicitArray: false, ignoreAttrs: true });
const body = result['w:document']['w:body'];
return Array.isArray(body['w:p']) ? body['w:p'] : [body['w:p']];
} catch (error) {
console.error('Error parsing XML:', error);
throw error;
}
};
const xmlContent = await parseXml(docXml);
const extractTags = (nodes) => {
const tags = new Set();
nodes.forEach((node) => {
if (node['w:r'] && Array.isArray(node['w:r'])) {
node['w:r'].forEach((r) => {
if (r['w:t'] && r['w:t']['#']) {
const matches = r['w:t']['#'].match(/\{[\w-]+\.}/g);
if (matches) {
matches.forEach((tag) => tags.add(tag));
}
}
});
}
});
return tags;
};
const tags = extractTags(xmlContent);
return Array.from(tags); // Ensure the return value is an array
} catch (error) {
console.error('Error in getTemplateParams:', error);
throw error;
}
}

View File

@ -0,0 +1 @@
export { default } from './plugin';

View File

@ -0,0 +1,29 @@
import { Plugin } from '@tachybase/server';
import { generate } from './actions/printTemplates';
export class PluginPrintTemplateServer extends Plugin {
async afterAdd() {}
async beforeLoad() {}
async load() {
// localhost:3000/api/printTemplates:generate
this.app.resourcer.define({
name: 'printTemplates',
actions: {
generate,
},
});
}
async install() {}
async afterEnable() {}
async afterDisable() {}
async remove() {}
}
export default PluginPrintTemplateServer;

View File

@ -3483,6 +3483,27 @@ importers:
specifier: ^6.11.2
version: 6.21.0(react-dom@18.3.1)(react@18.3.1)
packages/plugins/@tachybase/plugin-print-template:
dependencies:
'@tachybase/client':
specifier: workspace:*
version: link:../../../core/client
'@tachybase/server':
specifier: workspace:*
version: link:../../../core/server
'@tachybase/test':
specifier: workspace:*
version: link:../../../core/test
docxtemplater:
specifier: ^3.48.0
version: 3.48.0
pizzip:
specifier: ^3.1.7
version: 3.1.7
xml2js:
specifier: ^0.6.2
version: 0.6.2
packages/plugins/@tachybase/plugin-saml:
dependencies:
'@tachybase/actions':
@ -13928,7 +13949,6 @@ packages:
/@xmldom/xmldom@0.8.10:
resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==}
engines: {node: '>=10.0.0'}
dev: true
/@xtuc/ieee754@1.2.0:
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
@ -17432,6 +17452,13 @@ packages:
dependencies:
esutils: 2.0.3
/docxtemplater@3.48.0:
resolution: {integrity: sha512-eM/sSUIVZND/GeY7FCC8kL2QV9oEQZ/RNxfY9cC07JutpVMFC9EEBOk4ajs3mcN72gqzOUqrsdwXPmKTD9TyHA==}
engines: {node: '>=0.10'}
dependencies:
'@xmldom/xmldom': 0.8.10
dev: false
/dom-accessibility-api@0.5.16:
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
@ -22634,7 +22661,7 @@ packages:
dependencies:
debug: 3.2.7
iconv-lite: 0.4.24
sax: 1.3.0
sax: 1.4.1
transitivePeerDependencies:
- supports-color
dev: false
@ -23330,6 +23357,10 @@ packages:
/pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
/pako@2.1.0:
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
dev: false
/param-case@3.0.4:
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
dependencies:
@ -23710,6 +23741,12 @@ packages:
optionalDependencies:
nice-napi: 1.0.2
/pizzip@3.1.7:
resolution: {integrity: sha512-VemVeAQtdIA74AN1Fsd5OmbMbEeS4YOwwlcudgzvmUrOIOPrk1idYC5Tw5FUFq/I0c26ziNOw9z//iPmGfp1jA==}
dependencies:
pako: 2.1.0
dev: false
/pkg-dir@4.2.0:
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
engines: {node: '>=8'}
@ -26886,13 +26923,9 @@ packages:
immutable: 4.3.5
source-map-js: 1.2.0
/sax@1.3.0:
resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==}
/sax@1.4.1:
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
requiresBuild: true
optional: true
/saxes@5.0.1:
resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==}
@ -30356,7 +30389,7 @@ packages:
resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==}
engines: {node: '>=4.0.0'}
dependencies:
sax: 1.3.0
sax: 1.4.1
xmlbuilder: 11.0.1
dev: true
@ -30364,7 +30397,7 @@ packages:
resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==}
engines: {node: '>=4.0.0'}
dependencies:
sax: 1.3.0
sax: 1.4.1
xmlbuilder: 11.0.1
dev: true
@ -30372,14 +30405,12 @@ packages:
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
engines: {node: '>=4.0.0'}
dependencies:
sax: 1.3.0
sax: 1.4.1
xmlbuilder: 11.0.1
dev: true
/xmlbuilder@11.0.1:
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
engines: {node: '>=4.0'}
dev: true
/xmlbuilder@15.1.1:
resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}