Vue将内容生成为二维码,并将所有二维码下载为图片,同时支持批量下载(下载为ZIP),含解决一次性生成过多时页面崩溃解决办法

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>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值