在数智赛事中某场赛事需要导出全部的球员报告pdf和球星卡图片,需要一次性生成全部数据导出。
在生成img图片的时候遇到顶部白线问题使用截取1px解决
clip: {
x: boundingBox.x,
y: boundingBox.y + 1, // 去掉顶部1px
width: boundingBox.width,
height: boundingBox.height - 1 // 减去1px
}
完整代码如下
const express = require("express");
const axios = require("axios");
const puppeteer = require("puppeteer");
const fs = require("fs");
const path = require("path");
const FormData = require("form-data");
const imgList = require('./data/img.js');
const pdfList = require('./data/pdf.js');
module.exports = async (app) => {
const router = express.Router({
mergeParams: true, //合并url参数 导入父级参数到子级配置
});
const { default: Queue } = await import("queue");
// 创建一个任务队列
const screenshotQueue = new Queue({
concurrency: 1, // 同时处理一个任务
autostart: false, // 自动开始处理任务
});
// 定义生成截图的函数
const generateScreenshot = async (url, screenshotPath, retries = 3) => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// 设置页面视口尺寸,增加分辨率
await page.setViewport({ width: 744, height: 1045, deviceScaleFactor: 2 });
try {
await page.goto(url, { waitUntil: "networkidle0", timeout: 60000 }); // 增加导航超时时间到60秒
// 等待 class="front-block" 元素加载完毕
await page.waitForSelector(".front-block", { timeout: 60000 }); // 增加等待超时时间到60秒
// 等待所有图片加载完毕
await page.evaluate(async () => {
const images = Array.from(document.images);
await Promise.all(
images.map((img) => {
if (img.complete) return;
return new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
});
})
);
});
// 获取 class="front-block" 元素
const frontBlock = await page.$(".front-block");
if (frontBlock) {
// 获取元素的尺寸和位置
const boundingBox = await frontBlock.boundingBox();
// 截取该元素的截图,并使用 clip 选项去掉顶部白线
await page.screenshot({
path: screenshotPath,
type: 'jpeg',
quality: 100,
clip: {
x: boundingBox.x,
y: boundingBox.y + 1, // 去掉顶部1px
width: boundingBox.width,
height: boundingBox.height - 1 // 减去1px
}
});
// 关闭浏览器
await browser.close();
// 返回截图路径
return screenshotPath;
} else {
await browser.close();
throw new Error('Element with class "front-block" not found');
}
} catch (error) {
await browser.close();
if (retries > 0) {
console.log(`Retrying... (${3 - retries + 1})`);
return generateScreenshot(url, screenshotPath, retries - 1);
} else {
throw error;
}
}
};
app.post("/pdf/api/img", async (req, res) => {
try {
const mainList = imgList;
for (const item of mainList) {
const { id, competitionId, playerId, type, page, playerName, shirtNum,competionName } = item;
const url = `http://localhost:8080/#/starCardServe?token=176541f05cddc799da155fa8e3f06a2d&id=${id}&skinId=32¤tProfilePicture=undefined&competitionId=${competitionId}&playerId=${playerId}&type=${type}&&page=back`;
console.log('请求的页面', url);
// 创建 球星卡背面 文件夹路径
const screenshotDir = path.join(__dirname, '球星卡背面');
if (!fs.existsSync(screenshotDir)) {
fs.mkdirSync(screenshotDir, { recursive: true });
}
const screenshotPath = path.join(screenshotDir, `${competionName}-${playerName}-${shirtNum}-背面.png`);
// 添加任务到队列
screenshotQueue.push(async (cb) => {
try {
const res = await generateScreenshot(url, screenshotPath);
console.log("🚀 生成图片成功:", res);
cb(null, screenshotPath);
} catch (err) {
console.log("🚀 生成图片错误:", err.message);
cb(err);
}
});
}
// 检查队列是否已经在运行
if (!screenshotQueue.running) {
screenshotQueue.start((err, result) => {
if (err) {
console.error(`队列处理错误: ${err.message}`);
} else {
console.log(`队列处理完成: ${result}`);
}
});
}
// 立即返回响应
res.status(200).send({
code: "200",
message: "任务已添加到队列",
ok: true,
});
} catch (err) {
res.status(500).send({
code: 500,
message: err.message,
ok: false,
});
}
});
// 创建一个任务队列
const pdfQueue = new Queue({
concurrency: 1, // 同时处理一个任务
autostart: false, // 自动开始处理任务
});
// 定义生成PDF的函数
const generatePDF = async (url, pdfPath, retries = 3) => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
try {
await page.goto(url, { waitUntil: "networkidle0", timeout: 60000 }); // 增加导航超时时间到60秒
// 等待页面特定内容加载完毕
await page.waitForSelector(".player-report", { timeout: 60000 }); // 增加等待超时时间到60秒
// 设置页面尺寸
await page.setViewport({ width: 750, height: 1624 });
// 渲染成PDF
await page.pdf({
path: pdfPath,
width: '750px',
height: '1624px',
printBackground: true,
margin: { top: '0px', right: '0px', bottom: '0px', left: '0px' },
displayHeaderFooter: false,
});
await browser.close();
return pdfPath;
} catch (error) {
await browser.close();
if (retries > 0) {
console.log(`Retrying... (${3 - retries + 1})`);
return generatePDF(url, pdfPath, retries - 1);
} else {
throw error;
}
}
};
app.post("/pdf/api/pdf", async (req, res) => {
try {
const mainList = pdfList;
for (const item of mainList) {
const { playerId, registrationPlayerId, scheduleId, matchId, matchZoneId, playerName, shirtNum, competitionId, competionName, mId } = item;
const url = `http://localhost:8080/#/joyPlayerReport?playerId=${playerId}®istrationPlayerId=${registrationPlayerId}&scheduleId=${scheduleId}&page=all&matchId=${matchId}&matchZoneId=${matchZoneId}&userId=undefined&mId=${mId}`;
console.log('请求的页面', url);
// 创建 competition 文件夹路径
const competitionDir = path.join(__dirname, 'exportPdfs', competionName);
if (!fs.existsSync(competitionDir)) {
fs.mkdirSync(competitionDir, { recursive: true });
}
const pdfPath = path.join(competitionDir, `${competionName}-${shirtNum}-${playerName}.pdf`);
// 添加任务到队列
pdfQueue.push(async (cb) => {
try {
const pdfres = await generatePDF(url, pdfPath);
console.log("🚀 ~ pdfQueue.push ~ pdfres:", pdfres);
cb(null, pdfPath);
} catch (err) {
console.log("🚀 ~ pdfQueue.push ~ err:");
cb(err);
}
});
}
// 检查队列是否已经在运行
if (!pdfQueue.running) {
pdfQueue.start((err, result) => {
if (err) {
res.status(500).send({
code: 500,
message: err.message,
ok: false,
});
} else {
res.send({
code: 200,
entity: result, // 这里返回的是PDF的本地路径
message: "PDF生成成功",
ok: true,
});
}
});
} else {
res.status(200).send({
code: 200,
message: "任务已添加到队列",
ok: true,
});
}
} catch (err) {
res.status(500).send({
code: 500,
message: err.message,
ok: false,
});
}
});
// 错误处理函数
app.use(async (err, req, res, next) => {
res.status(err.statusCode || 500).send({
code: 500,
message: err.message,
ok: false,
});
});
};