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:
parent
6a151a136c
commit
49a8e2e534
@ -0,0 +1,2 @@
|
|||||||
|
/node_modules
|
||||||
|
/src
|
@ -0,0 +1 @@
|
|||||||
|
# @tachybase/plugin-print-template
|
2
packages/plugins/@tachybase/plugin-print-template/client.d.ts
vendored
Normal file
2
packages/plugins/@tachybase/plugin-print-template/client.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './dist/client';
|
||||||
|
export { default } from './dist/client';
|
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/client/index.js');
|
@ -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:*"
|
||||||
|
}
|
||||||
|
}
|
2
packages/plugins/@tachybase/plugin-print-template/server.d.ts
vendored
Normal file
2
packages/plugins/@tachybase/plugin-print-template/server.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './dist/server';
|
||||||
|
export { default } from './dist/server';
|
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/server/index.js');
|
@ -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;
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from './server';
|
||||||
|
export { default } from './server';
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { default } from './plugin';
|
@ -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;
|
@ -3483,6 +3483,27 @@ importers:
|
|||||||
specifier: ^6.11.2
|
specifier: ^6.11.2
|
||||||
version: 6.21.0(react-dom@18.3.1)(react@18.3.1)
|
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:
|
packages/plugins/@tachybase/plugin-saml:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tachybase/actions':
|
'@tachybase/actions':
|
||||||
@ -13928,7 +13949,6 @@ packages:
|
|||||||
/@xmldom/xmldom@0.8.10:
|
/@xmldom/xmldom@0.8.10:
|
||||||
resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==}
|
resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@xtuc/ieee754@1.2.0:
|
/@xtuc/ieee754@1.2.0:
|
||||||
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
|
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
|
||||||
@ -17432,6 +17452,13 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
esutils: 2.0.3
|
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:
|
/dom-accessibility-api@0.5.16:
|
||||||
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
|
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
|
||||||
|
|
||||||
@ -22634,7 +22661,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
iconv-lite: 0.4.24
|
iconv-lite: 0.4.24
|
||||||
sax: 1.3.0
|
sax: 1.4.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
dev: false
|
||||||
@ -23330,6 +23357,10 @@ packages:
|
|||||||
/pako@1.0.11:
|
/pako@1.0.11:
|
||||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
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:
|
/param-case@3.0.4:
|
||||||
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
|
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -23710,6 +23741,12 @@ packages:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
nice-napi: 1.0.2
|
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:
|
/pkg-dir@4.2.0:
|
||||||
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
|
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -26886,13 +26923,9 @@ packages:
|
|||||||
immutable: 4.3.5
|
immutable: 4.3.5
|
||||||
source-map-js: 1.2.0
|
source-map-js: 1.2.0
|
||||||
|
|
||||||
/sax@1.3.0:
|
|
||||||
resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==}
|
|
||||||
|
|
||||||
/sax@1.4.1:
|
/sax@1.4.1:
|
||||||
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
|
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
optional: true
|
|
||||||
|
|
||||||
/saxes@5.0.1:
|
/saxes@5.0.1:
|
||||||
resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==}
|
resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==}
|
||||||
@ -30356,7 +30389,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==}
|
resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
sax: 1.3.0
|
sax: 1.4.1
|
||||||
xmlbuilder: 11.0.1
|
xmlbuilder: 11.0.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@ -30364,7 +30397,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==}
|
resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
sax: 1.3.0
|
sax: 1.4.1
|
||||||
xmlbuilder: 11.0.1
|
xmlbuilder: 11.0.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@ -30372,14 +30405,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
|
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
sax: 1.3.0
|
sax: 1.4.1
|
||||||
xmlbuilder: 11.0.1
|
xmlbuilder: 11.0.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/xmlbuilder@11.0.1:
|
/xmlbuilder@11.0.1:
|
||||||
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/xmlbuilder@15.1.1:
|
/xmlbuilder@15.1.1:
|
||||||
resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}
|
resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}
|
||||||
|
Loading…
Reference in New Issue
Block a user