网页下载二维码压缩包_基于vue2、jszip与qrcode打包下载二维码_对比服务端生成压缩包
2025-11-03 15:06:52
如何基于jszip与qrcode在网页上实现下载二维码的压缩包,对比服务端生成有什么优劣势,本文会详细说明
558
现在有一个需求是在网页上生成指定的二维码,按照以前的逻辑是在服务端生成二维码,并且再把文件打包压缩
引入QRCode:
import * as QRCode from "qrcode";
const qrOptions={ // 二维码选项
width: 500, // 图片宽度
margin: 1, // 边距
color: {
dark: '#000000', // 前景色
light: '#FFFFFF00' // 背景色
}
};
await QRCode.toFile(filePath, "xxxx", qrOptions);
就可以生成指定文件到服务器目录,
如果需要生成自定义的海报,则是有两种方式:
- puppeteer
- 生成一个网页,将二维码与样式以网页的形式截图,保存图片
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可以自己绘制,就是需要一定的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>
