文章目录
0 注意事项:
1 当所需下载的二维码过多时,会很卡很卡,并且会导致浏览器崩溃,此时可以考虑采用 #2 / #3 / #4进行切片分批下载,里面添加了内存监测,内存占用超出后,会暂停生成,直到内存占用回到阈值。**
2 如果存在页面访问速度过慢问题,则需将npm包进行按需加载
// 注释掉以下代码
// import html2canvas from 'html2canvas'
// import JSZip from 'jszip'
// import { saveAs } from 'file-saver'
// 在需要的地方写入
const html2canvas = (await import('html2canvas')).default;
const { saveAs } = await import('file-saver');
const { saveAs } = await import('file-saver');
// 例如
const downloadAsZip = async () => {
const startTime = performance.now();
const BATCH_SIZE = 5; // 减少每批处理数量
let processedCount = 0;
try {
const JSZip = (await import('jszip')).default;
const html2canvas = (await import('html2canvas')).default;
const { saveAs } = await import('file-saver');
const zip = new JSZip();
const imgFolder = zip.folder("资产卡片二维码");
const currentBatchData = tableDataQr.value;
const containerEntries = currentBatchData.map(item => [item.cardCode, qrContainers.value[item.cardCode]]);
1 下载所需的包
vue-qr 生成二维码,html2canvas 将页面截取成图片,jszip将文件生成为zip, file-saver 下载文件
npm install vue-qr html2canvas jszip file-saver
2 直接下载二维码zip
2.1 效果图


2.2 完整代码
<template>
<div v-loading="loading">
<div class="qr-wrapper" v-for="item in tableDataQr" :key="item.id" :ref="el => setQrContainer(el, item.id)">
<vue-qr :text="item.qrUrl" :size="200"></vue-qr>
<div class="text-below">ID: {{ item.id }}</div>
</div>
<button @click="downloadQrZip">下载为ZIP</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import html2canvas from 'html2canvas'
import vueQr from 'vue-qr/src/packages/vue-qr.vue'
import JSZip from 'jszip'
import { saveAs } from 'file-saver'
const tableDataQr = ref([
{ id: 1, qrUrl: 'https://gitee.com/kuxiao-smile/node-nest' },
{ id: 2, qrUrl: 'https://gitee.com/kuxiao-smile/node-nest' },
{ id: 3, qrUrl: 'https://gitee.com/kuxiao-smile/node-nest' },
{ id: 4, qrUrl: 'https://gitee.com/kuxiao-smile/node-nest' },
{ id: 5, qrUrl: 'https://gitee.com/kuxiao-smile/node-nest' },
])
const qrText = ref("")
const qrContainers = ref({})
const loading = ref(false)
// 创建所有容器的 ref
const setQrContainer = (el, index) => {
if (el) qrContainers.value[index] = el
}
// 直接下载二维码zip
const downloadQrZip = async () => {
try {
const zip = new JSZip()
const imgFolder = zip.folder("二维码") // 二维码图片名称
const promises = []
// 确保所有DOM元素已加载
await new Promise(resolve => setTimeout(resolve, 100))
// 遍历收集到的所有容器
Object.entries(qrContainers.value).forEach(([index, container]) => {
promises.push(
html2canvas(container, {
backgroundColor: '#ffffff',
scale: 2,
logging: false, // 关闭日志减少内存
useCORS: true, // 允许跨域
removeContainer: true // 处理完成后移除DOM引用
}).then(canvas => {
return new Promise(resolve => {
canvas.toBlob(blob => {
if (!blob) {
console.error('生成Blob失败:', canvas)
resolve()
return
}
imgFolder.file(`二维码_${index}.jpg`, blob)
resolve()
}, 'image/jpeg', 0.92)
})
}).catch(err => {
console.error(`生成第${index}个二维码失败:`, err)
})
)
})
// 等待所有图片生成
const results = await Promise.allSettled(promises)
console.log('生成结果:', results)
// 检查是否生成了任何文件
if (Object.keys(imgFolder.files).length === 0) {
throw new Error('没有生成任何图片文件')
}
// 生成ZIP并下载
const content = await zip.generateAsync({ type: 'blob' })
saveAs(content, '二维码.zip')
} catch (error) {
console.error('生成ZIP失败:', error)
alert('生成ZIP失败: ' + error.message)
}
}
</script>
<style scoped>
.qr-wrapper {
display: flex;
flex-direction: column;
align-items: center;
margin: 20px auto;
padding: 20px;
background-color: white;
width: fit-content;
}
.text-below {
margin-top: 10px;
font-size: 18px;
font-weight: bold;
}
button {
margin-top: 20px;
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
3 直接下载二维码zip,添加以下功能:切片/每次切片后的判断是否继续生成/添加内存监测
3.1 效果图


3.2 完整代码
<template>
<div v-loading="loading">
<div class="qr-wrapper" v-for="item in tableDataQr" :key="item.id" :ref="el => setQrContainer(el, item.id)">
<vue-qr :text="item.qrUrl" :size="200"></vue-qr>
<div class="text-below">ID: {{ item.id }}</div>
</div>
<button @click="downloadQrZip">下载为ZIP</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import html2canvas from 'html2canvas'
import vueQr from 'vue-qr/src/packages/vue-qr.vue'
import JSZip from 'jszip'
import { saveAs } from 'file-saver'
const tableDataQr = ref([
{ id: 1, qrUrl: 'https://gitee.com/kuxiao-smile/node-nest' },
{ id: 2, qrUrl: 'https://gitee.com/kuxiao-smile/node-nest' },
{ id: 3, qrUrl: 'https://gitee.com/kuxiao-smile/node-nest' },
{ id: 4, qrUrl: 'https://gitee.com/kuxiao-smile/node-nest' },
{ id: 5, qrUrl: 'https://gitee.com/kuxiao-smile/node-nest' },
])
const qrText = ref("")
const qrContainers = ref({})
const loading = ref(false)
// 创建所有容器的 ref
const setQrContainer = (el, index) => {
if (el) qrContainers.value[index] = el
}
// 直接下载二维码,添加以下功能:切片/每次切片后的判断是否继续生成
const downloadQrZip = async () => {
const startTime = performance.now();
const BATCH_SIZE = 5; // 减少每批处理数量
let processedCount = 0;
try {
const zip = new JSZip();
const imgFolder = zip.folder("二维码");
const currentBatchData = tableDataQr.value;
const containerEntries = currentBatchData.map(item => [item.id, qrContainers.value[item.id]]);
console.log('开始分批生成二维码...', containerEntries);
// 内存监控函数
const checkMemory = () => {
if (window.performance && window.performance.memory) {
const usedMB = window.performance.memory.usedJSHeapSize / 1024 / 1024;
console.log(`内存使用: ${usedMB.toFixed(2)}MB`);
return usedMB > 500; // 超过500MB返回true
}
return false;
};
// 分批处理
for (let i = 0; i < containerEntries.length; i++) {
if (!checkContinueCondition()) {
console.log('条件不满足,停止生成')
return
}
if (checkMemory()) {
console.log('内存过高,暂停处理');
await new Promise(resolve => setTimeout(resolve, 3000));
}
const [index, container] = containerEntries[i];
try {
const canvas = await html2canvas(container, {
backgroundColor: '#ffffff',
scale: 2,
logging: false,
useCORS: true,
removeContainer: true
});
const blob = await new Promise(resolve => {
canvas.toBlob(blob => {
canvas.width = 1
canvas.height = 1
resolve(blob);
}, 'image/jpeg', 0.1);
});
if (blob) {
imgFolder.file(`二维码_${index}.jpg`, blob);
processedCount++;
console.log(`进度: ${processedCount}/${containerEntries.length}`);
}
// 每处理5个休息一次
if (processedCount % BATCH_SIZE === 0) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
} catch (err) {
console.error(`生成${index}二维码失败:`, err);
}
}
if (processedCount === 0) {
throw new Error('没有生成任何图片文件');
}
// 分片生成ZIP文件
const content = await zip.generateAsync({
type: 'blob',
streamFiles: true,
compression: 'DEFLATE',
compressionOptions: { level: 6 }
});
const endTime = performance.now();
const duration = (endTime - startTime) / 1000;
console.log(`处理完成,共${processedCount}个,耗时: ${duration.toFixed(2)}秒`);
// 添加生成后的判断逻辑
if (content.size > 0) {
console.log('ZIP文件生成成功,大小:', formatFileSize(content.size))
saveAs(content, '二维码.zip')
} else {
console.warn('生成的ZIP文件为空')
return {
success: false,
error: '生成的ZIP文件为空'
}
}
} catch (error) {
const endTime = performance.now()
const duration = (endTime - startTime) / 1000
console.log(`生成ZIP失败,已处理${processedCount}个,耗时: ${duration.toFixed(2)}秒`, error)
console.log(`生成ZIP失败(已处理${processedCount}个): ` + error.message);
}
}
// 条件判断
const checkContinueCondition = () => {
return true
}
// 格式化文件大小
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
</script>
<style scoped>
.qr-wrapper {
display: flex;
flex-direction: column;
align-items: center;
margin: 20px auto;
padding: 20px;
background-color: white;
width: fit-content;
}
.text-below {
margin-top: 10px;
font-size: 18px;
font-weight: bold;
}
button {
margin-top: 20px;
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
4 不再直接下载二维码,添加以下功能:切片/每次切片后的判断是否继续生成/添加内存监测/返回生成的二维码Blob、file格式信息,供后续上传服务器等操作
4.1 效果图


4.2 完整代码
<template>
<div v-loading="loading">
<div class="qr-wrapper" v-for="item in tableDataQr" :key="item.id" :ref="el => setQrContainer(el, item.id)">
<vue-qr :text="item.qrUrl" :size="200"></vue-qr>
<div class="text-below">ID: {{ item.id }}</div>
</div>
<button @click="downloadZip">下载为ZIP</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import html2canvas from 'html2canvas'
import vueQr from 'vue-qr/src/packages/vue-qr.vue'
import JSZip from 'jszip'
import { saveAs } from 'file-saver'
const tableDataQr = ref([
{ id: 1, qrUrl: 'https://gitee.com/kuxiao-smile/node-nest' },
{ id: 2, qrUrl: 'https://gitee.com/kuxiao-smile/node-nest' },
{ id: 3, qrUrl: 'https://gitee.com/kuxiao-smile/node-nest' },
{ id: 4, qrUrl: 'https://gitee.com/kuxiao-smile/node-nest' },
{ id: 5, qrUrl: 'https://gitee.com/kuxiao-smile/node-nest' },
])
const qrText = ref("")
const qrContainers = ref({})
const loading = ref(false)
// 创建所有容器的 ref
const setQrContainer = (el, index) => {
if (el) qrContainers.value[index] = el
}
const downloadZip = async () => {
try {
loading.value = true
const result = await downloadQrZip()
console.log(result, 'result');
} catch (error) {
console.log(error);
} finally {
loading.value = false
}
}
// 不再直接下载二维码
const downloadQrZip = async () => {
const startTime = performance.now();
const BATCH_SIZE = 5; // 减少每批处理数量
let processedCount = 0;
try {
const zip = new JSZip();
const imgFolder = zip.folder("二维码");
const currentBatchData = tableDataQr.value;
const containerEntries = currentBatchData.map(item => [item.id, qrContainers.value[item.id]]);
console.log('开始分批生成二维码...', containerEntries);
// 内存监控函数
const checkMemory = () => {
if (window.performance && window.performance.memory) {
const usedMB = window.performance.memory.usedJSHeapSize / 1024 / 1024;
console.log(`内存使用: ${usedMB.toFixed(2)}MB`);
return usedMB > 500; // 超过500MB返回true
}
return false;
};
// 分批处理
for (let i = 0; i < containerEntries.length; i++) {
if (!checkContinueCondition()) {
console.log('条件不满足,停止生成')
return
}
if (checkMemory()) {
console.log('内存过高,暂停处理');
await new Promise(resolve => setTimeout(resolve, 3000));
}
const [index, container] = containerEntries[i];
try {
const canvas = await html2canvas(container, {
backgroundColor: '#ffffff',
scale: 2,
logging: false,
useCORS: true,
removeContainer: true
});
const blob = await new Promise(resolve => {
canvas.toBlob(blob => {
canvas.width = 1
canvas.height = 1
resolve(blob);
}, 'image/jpeg', 0.1);
});
if (blob) {
imgFolder.file(`二维码_${index}.jpg`, blob);
processedCount++;
console.log(`进度: ${processedCount}/${containerEntries.length}`);
}
// 每处理5个休息一次
if (processedCount % BATCH_SIZE === 0) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
} catch (err) {
console.error(`生成${index}二维码失败:`, err);
}
}
if (processedCount === 0) {
throw new Error('没有生成任何图片文件');
}
// 分片生成ZIP文件
const content = await zip.generateAsync({
type: 'blob',
streamFiles: true,
compression: 'DEFLATE',
compressionOptions: { level: 6 }
});
const endTime = performance.now();
const duration = (endTime - startTime) / 1000;
console.log(`处理完成,共${processedCount}个,耗时: ${duration.toFixed(2)}秒`);
// 添加生成后的判断逻辑
if (content.size > 0) {
console.log('ZIP文件生成成功,大小:', formatFileSize(content.size))
// 创建实际文件对象
// 如果此时生成时间不存在, 则以当前时间
const fileName = `二维码.zip`
const file = new File([content], fileName, {
type: 'application/zip',
lastModified: Date.now()
})
const fileInfo = {
success: true,
file: file,
blob: content,
count: processedCount,
duration: duration.toFixed(2),
size: formatFileSize(content.size),
fileName: fileName
}
return fileInfo
} else {
console.warn('生成的ZIP文件为空')
return {
success: false,
error: '生成的ZIP文件为空'
}
}
} catch (error) {
const endTime = performance.now()
const duration = (endTime - startTime) / 1000
console.log(`生成ZIP失败,已处理${processedCount}个,耗时: ${duration.toFixed(2)}秒`, error)
console.log(`生成ZIP失败(已处理${processedCount}个): ` + error.message);
}
}
// 条件判断
const checkContinueCondition = () => {
return true
}
// 格式化文件大小
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
</script>
<style scoped>
/* 保持原有样式不变 */
.qr-wrapper {
display: flex;
flex-direction: column;
align-items: center;
margin: 20px auto;
padding: 20px;
background-color: white;
width: fit-content;
}
.text-below {
margin-top: 10px;
font-size: 18px;
font-weight: bold;
}
button {
margin-top: 20px;
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
5 下载url / 本地已经生成好的二维码,添加以下功能:切片/每次切片后的判断是否继续生成/添加内存监测/返回生成的二维码Blob、file格式信息,供后续上传服务器等操作
<template>
<div v-loading="loading">
<div class="qr-wrapper" v-for="item in tableDataQr" :key="item.id" :ref="el => setQrContainer(el, item.id)">
<vue-qr :text="item.qrUrl" :size="200"></vue-qr>
<div class="text-below">ID: {{ item.id }}</div>
</div>
<button @click="downloadSingleImageAsZip">下载为ZIP</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import html2canvas from 'html2canvas'
import vueQr from 'vue-qr/src/packages/vue-qr.vue'
// import JSZip from 'jszip'
import { saveAs } from 'file-saver'
import qrIcon from '@/assets/qr-icon.png'
const tableDataQr = ref([
{ id: 1, qrUrl: 'https://gitee.com/kuxiao-smile/node-nest', qrCode: qrIcon },
{ id: 2, qrUrl: 'https://gitee.com/kuxiao-smile/node-nest', qrCode: qrIcon },
{ id: 3, qrUrl: 'https://gitee.com/kuxiao-smile/node-nest', qrCode: qrIcon },
{ id: 4, qrUrl: 'https://gitee.com/kuxiao-smile/node-nest', qrCode: qrIcon },
{ id: 5, qrUrl: 'https://gitee.com/kuxiao-smile/node-nest', qrCode: qrIcon },
])
const qrText = ref("")
const qrContainers = ref({})
const loading = ref(false)
// 创建所有容器的 ref
const setQrContainer = (el, index) => {
if (el) qrContainers.value[index] = el
}
const downloadZip = async () => {
try {
loading.value = true
const result = await downloadQrZip()
console.log(result, 'result');
} catch (error) {
console.log(error);
} finally {
loading.value = false
}
}
// 条件判断
const checkContinueCondition = () => {
return true
}
// 格式化文件大小
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
// 将多个 url 图片放入zip
const downloadSingleImageAsZip = async () => {
const startTime = performance.now();
try {
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
const imgFolder = zip.folder("资产卡片二维码");
// 分批处理图片下载(每批100个)
const BATCH_SIZE = 100;
for (let i = 0; i < tableDataQr.value.length; i += BATCH_SIZE) {
const batch = tableDataQr.value.slice(i, i + BATCH_SIZE);
// 处理当前批次
await Promise.all(batch.map(async item => {
try {
const response = await fetch(item.qrCode);
if (!response.ok) throw new Error(`下载失败: ${item.qrCode}`);
const blob = await response.blob();
imgFolder.file(`二维码_${item.id}.jpg`, blob);
} catch (error) {
console.error(`下载二维码_${item.id}二维码失败:`, error);
}
}));
// 每批处理完休息一下
await new Promise(resolve => setTimeout(resolve, 150));
}
// 生成ZIP文件
const content = await zip.generateAsync({
type: 'blob',
streamFiles: true,
compression: 'DEFLATE',
compressionOptions: { level: 6 }
});
// 验证ZIP文件有效性
if (!content || content.size === 0) {
throw new Error('生成的ZIP文件为空');
}
const endTime = performance.now();
const duration = (endTime - startTime) / 1000;
// console.log(`处理完成,耗时: ${duration.toFixed(2)}秒`);
// 添加生成后的判断逻辑
if (content.size > 0) {
// console.log('ZIP文件生成成功,大小:', formatFileSize(content.size))
// 创建实际文件对象
// 如果此时生成时间不存在, 则以当前时间
const fileName = `二维码.zip`
const file = new File([content], fileName, {
type: 'application/zip',
lastModified: Date.now()
})
const fileInfo = {
success: true,
file: file,
blob: content,
duration: duration.toFixed(2),
size: formatFileSize(content.size),
fileName: fileName
}
saveAs(content, '二维码.zip');
return fileInfo
} else {
console.warn('生成的ZIP文件为空')
return {
success: false,
error: '生成的ZIP文件为空'
}
}
} catch (error) {
console.error('生成ZIP失败:', error);
return {
success: false,
error: error.message
};
}
}
</script>
<style scoped>
/* 保持原有样式不变 */
.qr-wrapper {
display: flex;
flex-direction: column;
align-items: center;
margin: 20px auto;
padding: 20px;
background-color: white;
width: fit-content;
}
.text-below {
margin-top: 10px;
font-size: 18px;
font-weight: bold;
}
button {
margin-top: 20px;
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
,含解决一次性生成过多时页面崩溃解决办法&spm=1001.2101.3001.5002&articleId=150532155&d=1&t=3&u=b14f9a8374eb4f3681c1961047747e8e)
5162

被折叠的 条评论
为什么被折叠?



