Vue将内容生成为二维码,并将所有二维码下载为图片,同时支持批量下载(下载为ZIP),含解决一次性生成过多时页面崩溃解决办法
文章目录
- 1 下载所需的包
- 2 直接下载二维码zip
- 2.1 效果图
- 2.2 完整代码
- 3 直接下载二维码zip,添加以下功能:切片/每次切片后的判断是否继续生成/添加内存监测
- 3.1 效果图
- 3.2 完整代码
- 4 不再直接下载二维码,添加以下功能:切片/每次切片后的判断是否继续生成/添加内存监测/返回生成的二维码Blob、file格式信息,供后续上传服务器等操作
- 4.1 效果图
- 4.2 完整代码
注意事项:当所需下载的二维码过多时,会很卡很卡,并且会导致浏览器崩溃,此时可以考虑采用 #2 / #3 进行切片分批下载,里面添加了内存监测,内存占用超出后,会暂停生成,直到内存占用回到阈值。
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 = 1canvas.height = 1resolve(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) / 1000console.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 = 1024const 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 = trueconst 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 = 1canvas.height = 1resolve(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) / 1000console.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 = 1024const 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>