现在有一个需求是在网页上生成指定的二维码,按照以前的逻辑是在服务端生成二维码,并且再把文件打包压缩
引入QRCode:
import * as QRCode from "qrcode"; const qrOptions={ // 二维码选项 width: 500, // 图片宽度 margin: 1, // 边距 color: { dark: '#000000', // 前景色 light: '#FFFFFF00' // 背景色 } }; await QRCode.toFile(filePath, "xxxx", qrOptions);
就可以生成指定文件到服务器目录,
如果需要生成自定义的海报,则是有两种方式:
import * as puppeteer from "puppeteer"; const browser = await puppeteer.launch({ headless: 'new', // 使用无头模式 args: ['--no-sandbox', '--disable-setuid-sandbox'] // 沙箱配置 }); await this.captureScreenshot(browser, { url:"xxx",name:"test" }, { urls: [],// 要截图的网页列表 outputDir: './public/tempZipImg', // 截图保存目录 outputZipDir: './public/zip', // 截图保存目录 zipFileName: 'Charging QR Code (with Details).zip', // 压缩文件名 viewport: { width: 1920, height: 1080 }, // 视口大小 fullPage: true, // 是否截取整个页面 timeout: 30000, // 页面加载超时时间(毫秒) qrOptions: { // 二维码选项 width: 500, // 图片宽度 margin: 1, // 边距 color: { dark: '#000000', // 前景色 light: '#FFFFFF00' // 背景色 } } }); /** * @description: * @param {*} browser 无头浏览器对象 * @param {*} urlInfo.url 二维码内容,此时是一个url * @param {*} urlInfo.name 二维码的名字 * @param {*} config 部分配置 * @return {*} * @Date: 2025-06-18 13:38:24 */ async captureScreenshot(browser, urlInfo, config) { const page = await browser.newPage(); try { // 设置视口大小 await page.setViewport(config.viewport); // 导航到目标URL await page.goto(urlInfo.url, { waitUntil: 'networkidle2', timeout: config.timeout }); // 截图路径 const screenshotName = urlInfo.name+".png"; const screenshotPath = join(config.outputDir, screenshotName); // 截取屏幕 await page.screenshot({ path: screenshotPath, fullPage: config.fullPage }); console.log('截图完成!'); return { path: screenshotPath, name: screenshotName }; } catch (error) { // 返回null表示该URL截图失败 return null; } finally { // 关闭页面 await page.close(); } }
这样的劣势就是每一张图片需要2-3s左右的时间,并且需要耗费服务器的性能
使用canvas可以自己绘制,就是需要一定的canvas基础
import * as QRCode from "qrcode"; import { createCanvas, loadImage } from 'canvas'; async canvasCode(urlInfo, config) { const options = { title: urlInfo.name, subtitle: "2", text: urlInfo.url, footer: "1", logoUrl: "./public/images/qrtest.png" } const canvasWidth = 500; const canvasHeight = 500; const canvas = createCanvas(canvasWidth, canvasHeight); const ctx = canvas.getContext('2d'); // 绘制背景 ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvasWidth, canvasHeight); // 绘制标题 ctx.fillStyle = '#333333'; ctx.font = 'bold 24px Arial'; ctx.textAlign = 'center'; ctx.fillText(options.title, canvasWidth / 2, 50); // 绘制副标题 ctx.font = '16px Arial'; ctx.fillText(options.subtitle, canvasWidth / 2, 85); // 生成二维码 const qrCodeSize = 280; const qrCodeBuffer = await QRCode.toBuffer(options.text, { width: qrCodeSize, margin: 1 }); const qrCodeImage = await loadImage(qrCodeBuffer); // 绘制二维码 const qrCodeX = (canvasWidth - qrCodeSize) / 2; const qrCodeY = 120; ctx.drawImage(qrCodeImage, qrCodeX, qrCodeY, qrCodeSize, qrCodeSize); // 绘制底部文本 ctx.font = '14px Arial'; ctx.fillText(options.footer, canvasWidth / 2, canvasHeight - 30); // 加载并绘制logo if (options.logoUrl) { try { const logoImage = await loadImage(join(options.logoUrl)); const logoSize = 60; const logoX = (canvasWidth - logoSize) / 2; const logoY = canvasHeight - 100; // 绘制logo圆形背景 ctx.beginPath(); ctx.arc(logoX + logoSize / 2, logoY + logoSize / 2, logoSize / 2, 0, Math.PI * 2); ctx.fillStyle = '#ffffff'; ctx.fill(); ctx.closePath(); // 绘制logo ctx.save(); ctx.beginPath(); ctx.arc(logoX + logoSize / 2, logoY + logoSize / 2, logoSize / 2 - 2, 0, Math.PI * 2); ctx.clip(); ctx.drawImage(logoImage, logoX, logoY, logoSize, logoSize); ctx.restore(); } catch (error) { console.error('Failed to load logo:', error); // 绘制默认logo占位符 const logoSize = 60; const logoX = (canvasWidth - logoSize) / 2; const logoY = canvasHeight - 100; ctx.fillStyle = '#eeeeee'; ctx.beginPath(); ctx.arc(logoX + logoSize / 2, logoY + logoSize / 2, logoSize / 2, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#999999'; ctx.font = '12px Arial'; ctx.fillText('LOGO', logoX + logoSize / 2, logoY + logoSize / 2 + 4); } } const fileName = urlInfo.name+".png"; const filePath = join(config.outputDir, fileName); fs.writeFileSync(filePath, canvas.toBuffer('image/png')); return { path: filePath, name: fileName } }
生成完图片之后,需要打包压缩
import * as archiver from "archiver"; /** * @description: * @param {*} screenshotFiles 截图文件目录数组 * @param {*} zipPath 输出的目录 * @param {*} name * @return {*} * @Date: 2025-06-18 13:44:40 */ async fnCreateZipArchive(screenshotFiles, zipPath, name) { const output = fs.createWriteStream(zipPath); const archive = archiver('zip', { zlib: { level: 9 } }); // 最高压缩级别 // 监听压缩过程中的事件 output.on('close', () => { }); archive.on('error', (err) => { console.error('压缩过程中出错:', err); throw err; }); // 将所有截图添加到压缩包 archive.pipe(output); for (const file of screenshotFiles) { if (file) { archive.file(file.path, { name: file.name }); } } // 完成压缩 await archive.finalize(); return zipPath; }
上诉都是服务器生成图片压缩的逻辑,下面是基于vue2页面的生成压缩包,好处是减少服务器压力
<template> <a-dropdown> <a-menu slot="overlay"> <a-menu-item key="1" @click=fnDownZipQRCode(1)> 下载普通二维码 </a-menu-item> <a-menu-item key="2" @click=fnDownZipQRCode(2)> 下载样式二维码 </a-menu-item> </a-menu> <pButton> <div class="downQRCodeBox"> 下载二维码 <a-icon type="down" /> <div class="downQRCodeBody"> <div v-for="(qrcode, index) in qrcodeImages" :key="index" class="qrcode-item"> <div :id="'qrcode-' + index"></div> </div> </div> </div> </pButton> </a-dropdown> </template> <script> import QRCode from 'qrcode'; import JSZip from 'jszip'; import { saveAs } from 'file-saver'; import { formatinvalidChars } from "@/common-components/utils/util"; export default { name: "DownQRCodeButton", components: {}, props: { }, data() { return { qrcodeImages: [], sStationName: "", qrOptions: { width: 500, color: { dark: '#000000', light: '#ffffff00',//透明背景 }, } }; }, methods: { async fnDownZipQRCode(type) { try { let res = { data:{ name:"名称", aQRcode:['xxx:yyy','xxx:yyy'] } } if (res.success) { this.sStationName = res.data.name ? (formatinvalidChars(res.data.name) + " ") : ""; this.qrcodeImages = res.data.aQRcode; if(res.data.aQRcode.length==0){ //需要提示 return } this.generateQRCodes(type); } else { console.error(res); } } catch (error) { if (error && error.code) { //提示错误 } } }, // 生成二维码 async generateQRCodes(type) { await new Promise(resolve => setTimeout(resolve, 100)); try { for (let i = 0; i < this.qrcodeImages.length; i++) { const content = this.qrcodeImages[i]; const qrcodeId = "qrcode-"+i; // 生成二维码 await this.generateQRCode(qrcodeId, content, type); } await this.downloadZip(type); } catch (error) { console.error('生成二维码失败:', error); } finally { } }, // 生成单个二维码 generateQRCode(elementId, content, type = 1) { return new Promise(async (resolve, reject) => { // 确保元素存在 const container = document.getElementById(elementId); if (!container) { reject(new Error("容器" +elementId+ "不存在")); return; } // 清空容器并创建新的Canvas container.innerHTML = ''; if (type == 2) { // const canvas = document.createElement('canvas'); const canvas = await this.generateCombinedCanvas( 'https://lihuanting.com/upload/1617020718890.png', // Logo URL '公司名称', // 标题 content, // 二维码内容 '扫描二维码访问网站\n2023年10月1日' // 底部文本(多行) ); console.log('canvas: ', canvas); container.appendChild(canvas); resolve(); } else { const canvas = document.createElement('canvas'); container.appendChild(canvas); // 生成二维码 QRCode.toCanvas(canvas, content, { width: this.qrOptions.width, margin: 1, color: this.qrOptions.color }, (error) => { if (error) { reject(error); } else { resolve(); } }); } }); }, async generateCombinedCanvas(logoUrl, title, qrContent, bottomText) { console.log('logoUrl, title, qrContent, bottomText: ', logoUrl, title, qrContent, bottomText); // 创建Canvas const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); // 设置尺寸(可根据需要调整) const width = 500; const logoSize = 80; const qrSize = 100; const padding = 20; // 计算总高度 const height = padding * 4 + logoSize + qrSize + 60; // 额外空间用于文本 canvas.width = width; canvas.height = height; console.log('height: ', height); console.log('width: ', width); // 清空背景 ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, width, height); // 1. 绘制Logo(圆形裁剪) try { const logoImg = await this.loadImage(logoUrl); const logoX = (width - logoSize) / 2; const logoY = padding; // // 绘制Logo ctx.drawImage(logoImg, logoX, logoY, logoSize, logoSize); } catch (error) { console.log('error: ', error); // Logo加载失败时绘制默认图标 const logoX = (width - logoSize) / 2; const logoY = padding; ctx.fillStyle = '#eeeeee'; ctx.beginPath(); ctx.arc(logoX + logoSize / 2, logoY + logoSize / 2, logoSize / 2, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#999999'; ctx.font = '16px Arial'; ctx.textAlign = 'center'; ctx.fillText('LOGO', width / 2, padding + logoSize / 2 + 6); } // 2. 绘制标题文本 ctx.font = 'bold 20px Arial'; ctx.fillStyle = '#333333'; ctx.textAlign = 'center'; ctx.fillText(title, width / 2, padding * 2 + logoSize); // 3. 生成并绘制二维码 const qrX = (width - qrSize) / 2; const qrY = padding * 3 + logoSize + 20; let qrCodeImage = await new Promise((resolve, reject) => { QRCode.toDataURL(qrContent, { width: qrSize, }, (error, res) => { console.log('error,res: ', error, res); if (error) reject(error); var img = new Image(); img.onload = function (e) { console.log('e: ', e); // 图像加载完成后,将其绘制到画布上 ctx.drawImage(img, qrX, qrY); // 这里可以指定绘制的位置和大小 resolve(res) }; img.src = res; // 设置图像的源为数据URL }) }); // 4. 绘制底部文本 ctx.font = '14px Arial'; ctx.fillStyle = '#666666'; // 支持多行文本(用\n分隔) const lines = bottomText.split('\n'); lines.forEach((line, index) => { ctx.fillText( line, width / 2, qrY + qrSize + padding + 20 * (index + 1) ); }); return canvas; }, loadImage(url) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = reject; img.src = url; }); }, // 下载压缩包 async downloadZip(type) { try { const zip = new JSZip(); const folder = zip.folder(null); // 添加每个二维码到压缩包 for (let i = 0; i < this.qrcodeImages.length; i++) { const content = this.qrcodeImages[i]; let [equipmentNo, sequence] = content.split(":"); const qrcodeId = "qrcode-"+i; const canvas = document.getElementById(qrcodeId).querySelector('canvas'); if (canvas) { const imgData = canvas.toDataURL('image/png').split(',')[1]; const fileName = equipmentNo+" - "+sequence+".png";//二维码文件名:{桩编号} - {枪口号} folder.file(fileName, imgData, { base64: true }); } } // 生成压缩包并下载 const extFileName = type == 1 ? "Charging QR Code" : "Charging QR Code (with Details)"; const zipName = this.sStationName+extFileName+".zip";//文件名: zip.generateAsync({ type: 'blob' }, (metadata) => { }).then((content) => { saveAs(content, zipName); }); } catch (error) { console.error('下载压缩包失败:', error); } finally { this.qrcodeImages = []; } }, } }; </script> <style lang="less" scoped> .downQRCodeBox { position: relative; .downQRCodeBody { width: 1px; height: 1px; position: absolute; right: 0; top: 0; background: red; z-index: 999999; overflow: hidden; } } </style>