Forráskód Böngészése

Add 添加二维码生成组件,使用vue3-next-qrcode:4.0.0

Yue 5 napja
szülő
commit
61a8ab7d46

+ 1 - 0
UI/VAP_V3.VUE/package.json

@@ -47,6 +47,7 @@
 		"vue-i18n": "11.1.12",
 		"vue-json-pretty": "2.6.0",
 		"vue-router": "4.6.3",
+		"vue3-next-qrcode": "^4.0.0",
 		"vue3-treeselect-ts": "^0.0.4",
 		"vxe-table": "4.17.7",
 		"zeebe-bpmn-moddle": "1.0.0"

+ 940 - 0
UI/VAP_V3.VUE/src/components/qrcode/API_REFERENCE.md

@@ -0,0 +1,940 @@
+# 二维码组件 API 参考
+
+> **版本**: v2.0.0 | **更新时间**: 2026-04-13
+
+---
+
+## 📦 VbQrGen - 单个二维码组件
+
+### 基础用法
+
+```vue
+<template>
+  <VbQrGen 
+    ref="qrRef"
+    v-model="text"
+    title="我的二维码"
+    :size="200"
+    @success="handleSuccess"
+    @error="handleError"
+  />
+  <el-button @click="generate">生成</el-button>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import VbQrGen from '@/components/qrcode/VbQrGen.vue'
+
+const qrRef = ref<InstanceType<typeof VbQrGen>>()
+const text = ref('https://example.com')
+
+function generate() {
+  qrRef.value?.generateQRCode()
+}
+
+function handleSuccess(dataURL) {
+  console.log('生成成功', dataURL)
+}
+
+function handleError(error) {
+  console.error('生成失败', error)
+}
+</script>
+```
+
+---
+
+### Props
+
+| 属性                  | 类型                | 默认值            | 说明                         |
+| --------------------- | ------------------- | ----------------- | ---------------------------- |
+| **modelValue**        | `string`            | `""`              | 二维码文本内容(v-model)    |
+| **title**             | `string`            | `""`              | 标题文本                     |
+| **titlePosition**     | `"top" \| "bottom"` | `"top"`           | 标题位置                     |
+| **titleStyle**        | `object`            | `undefined`       | 标题自定义样式               |
+| **size**              | `number`            | `200`             | 二维码尺寸(像素)           |
+| **margin**            | `number`            | `12`              | 二维码边距(像素)           |
+| **colorDark**         | `string`            | `"#000000"`       | 深色块颜色                   |
+| **colorLight**        | `string`            | `"#ffffff"`       | 浅色块颜色                   |
+| **correctLevel**      | `0 \| 1 \| 2 \| 3`  | `1`               | 纠错等级 (L/M/Q/H)           |
+| **logoImage**         | `string`            | `undefined`       | Logo 图片地址                |
+| **logoScale**         | `number`            | `0.4`             | Logo 相对比例 (0-1)          |
+| **logoMargin**        | `number`            | `6`               | Logo 边距(像素)            |
+| **logoCornerRadius**  | `number`            | `8`               | Logo 圆角(像素)            |
+| **backgroundImage**   | `string`            | `undefined`       | 背景图片地址                 |
+| **backgroundDimming** | `string`            | `"rgba(0,0,0,0)"` | 背景遮罩颜色                 |
+| **gifBackgroundURL**  | `string`            | `undefined`       | GIF 背景图片                 |
+| **whiteMargin**       | `boolean`           | `true`            | 白色边距                     |
+| **autoColor**         | `boolean`           | `true`            | 自动计算浅色                 |
+| **dotScale**          | `number`            | `1`               | 定位点缩放 (0-1),越小越精致 |
+| **loadingText**       | `string`            | `"生成中..."`     | 加载提示文本                 |
+| **errorText**         | `string`            | `"生成失败"`      | 错误提示文本                 |
+
+**纠错等级说明:**
+- `0` = L (7% 容错)
+- `1` = M (15% 容错) ⭐ 推荐
+- `2` = Q (25% 容错)
+- `3` = H (30% 容错)
+
+**dotScale 效果:**
+- `0.2` - 超精致定位点
+- `0.35` - 精致定位点 ⭐ 推荐
+- `0.5` - 适中
+- `0.8` - 传统风格
+- `1.0` - 默认大小
+
+---
+
+### Events
+
+| 事件名                | 参数                                            | 触发时机       |
+| --------------------- | ----------------------------------------------- | -------------- |
+| **update:modelValue** | `(value: string)`                               | v-model 更新时 |
+| **success**           | `(dataURL: ArrayBuffer \| string \| undefined)` | 二维码生成成功 |
+| **error**             | `(error: unknown)`                              | 二维码生成失败 |
+
+---
+
+### Exposed Methods
+
+通过 `ref` 访问的方法:
+
+```typescript
+interface VbQrGenExposed {
+  // ✅ 核心方法 - 返回 Promise
+  generateQRCode(options?: QRCodeGenerateOptions): Promise<ArrayBuffer | string | undefined>
+  clearQRCode(): void
+  downloadQRCode(fileName?: string): Promise<void>
+  printQRCode(title?: string): Promise<void>
+  getQRCodeDataURL(): string | null
+  
+  // ✅ 状态获取
+  getQRCodeState(): {
+    url: string | ArrayBuffer | undefined
+    loading: boolean
+    error: unknown
+  }
+}
+```
+
+#### generateQRCode(options?)
+
+生成二维码。
+
+**参数:**
+```typescript
+interface QRCodeGenerateOptions {
+  text?: string              // 文本内容(可选,默认使用 modelValue)
+  size?: number              // 尺寸
+  margin?: number            // 边距
+  correctLevel?: 0 | 1 | 2 | 3  // 纠错等级
+  maskPattern?: number       // 掩码图案 (0-7)
+  version?: number           // 版本 (1-40)
+  colorDark?: string         // 深色
+  colorLight?: string        // 浅色
+  autoColor?: boolean        // 自动计算浅色
+  logoImage?: string         // Logo
+  logoScale?: number         // Logo 比例
+  logoMargin?: number        // Logo 边距
+  logoCornerRadius?: number  // Logo 圆角
+  backgroundImage?: string   // 背景图
+  backgroundDimming?: string // 背景遮罩
+  gifBackgroundURL?: string  // GIF 背景
+  whiteMargin?: boolean      // 白色边距
+  dotScale?: number          // 定位点缩放
+}
+```
+
+**返回值:** `Promise<dataURL>`
+
+**示例:**
+```typescript
+// 基础用法 - 使用 modelValue
+qrRef.value?.generateQRCode()
+
+// 自定义配置
+qrRef.value?.generateQRCode({
+  text: 'https://example.com',
+  size: 300,
+  colorDark: '#1677ff',
+  dotScale: 0.35
+})
+
+// 链式调用
+qrRef.value?.generateQRCode()
+  .then((dataURL) => console.log('成功', dataURL))
+  .catch((error) => console.error('失败', error))
+```
+
+---
+
+#### clearQRCode()
+
+清除当前二维码。
+
+**示例:**
+```typescript
+qrRef.value?.clearQRCode()
+```
+
+---
+
+#### downloadQRCode(fileName?)
+
+下载二维码为 PNG 图片。
+
+**参数:**
+- `fileName`: 文件名(不含扩展名),默认 `"qrcode"`
+
+**返回值:** `Promise<void>`
+
+**示例:**
+```typescript
+qrRef.value?.downloadQRCode('my-qrcode')
+  .then(() => console.log('下载成功'))
+  .catch((error) => console.error('下载失败', error))
+```
+
+---
+
+#### printQRCode(title?)
+
+打印二维码(打开打印窗口)。
+
+**参数:**
+- `title`: 打印标题,默认使用组件的 `title` prop 或 `"二维码"`
+
+**返回值:** `Promise<void>`
+
+**示例:**
+```typescript
+qrRef.value?.printQRCode('产品二维码')
+  .then(() => console.log('打印窗口已打开'))
+  .catch((error) => console.error('打印失败', error))
+```
+
+---
+
+#### getQRCodeDataURL()
+
+获取二维码的 Data URL。
+
+**返回值:** `string | null`
+
+**示例:**
+```typescript
+const dataURL = qrRef.value?.getQRCodeDataURL()
+if (dataURL) {
+  console.log('Data URL:', dataURL.substring(0, 50) + '...')
+}
+```
+
+---
+
+#### getQRCodeState()
+
+获取二维码当前状态。
+
+**返回值:**
+```typescript
+{
+  url: string | ArrayBuffer | undefined  // 二维码数据
+  loading: boolean                        // 是否正在生成
+  error: unknown                          // 错误信息
+}
+```
+
+**示例:**
+```typescript
+const state = qrRef.value?.getQRCodeState()
+console.log('加载状态:', state.loading)
+console.log('错误信息:', state.error)
+```
+
+---
+
+### Slots
+
+| 插槽名      | 作用域参数            | 说明             |
+| ----------- | --------------------- | ---------------- |
+| **title**   | -                     | 自定义标题       |
+| **qrcode**  | `{ qrcodeURL, size }` | 自定义二维码显示 |
+| **loading** | -                     | 自定义加载状态   |
+| **error**   | `{ error }`           | 自定义错误状态   |
+
+**示例:**
+```vue
+<VbQrGen v-model="text">
+  <!-- 自定义标题 -->
+  <template #title>
+    <h2 style="color: blue">自定义标题</h2>
+  </template>
+  
+  <!-- 自定义二维码显示 -->
+  <template #qrcode="{ qrcodeURL, size }">
+    <img :src="qrcodeURL" :style="{ width: size + 'px' }" />
+  </template>
+  
+  <!-- 自定义加载状态 -->
+  <template #loading>
+    <div class="custom-loading">加载中...</div>
+  </template>
+  
+  <!-- 自定义错误状态 -->
+  <template #error="{ error }">
+    <div class="custom-error">生成失败: {{ error }}</div>
+  </template>
+</VbQrGen>
+```
+
+---
+
+## 📦 VbQrMulti - 批量二维码组件
+
+### 基础用法
+
+```vue
+<template>
+  <VbQrMulti
+    ref="multiRef"
+    v-model:items="qrItems"
+    :columns="4"
+    @generate-all-complete="handleComplete"
+  />
+  <el-button @click="generateAll">批量生成</el-button>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import VbQrMulti from '@/components/qrcode/VbQrMulti.vue'
+import type { QRCodeItem } from '@/components/qrcode/VbQrMulti.vue'
+
+const multiRef = ref<InstanceType<typeof VbQrMulti>>()
+const qrItems = ref<QRCodeItem[]>([
+  { id: '1', text: 'https://a.com', title: '产品 A' },
+  { id: '2', text: 'https://b.com', title: '产品 B' }
+])
+
+function generateAll() {
+  multiRef.value?.generateAll()
+    .then((result) => {
+      console.log(`成功: ${result.success}, 失败: ${result.fail}`)
+    })
+}
+
+function handleComplete(result: { success: number; fail: number }) {
+  console.log('批量生成完成', result)
+}
+</script>
+```
+
+---
+
+### QRCodeItem 类型
+
+```typescript
+interface QRCodeItem {
+  id: string                    // 唯一标识
+  text: string                  // 二维码文本
+  title?: string                // 标题(可选)
+  options?: QRCodeGenerateOptions  // 自定义配置(可选)
+}
+```
+
+---
+
+### Props
+
+| 属性                  | 类型                 | 默认值      | 说明                  |
+| --------------------- | -------------------- | ----------- | --------------------- |
+| **items**             | `QRCodeItem[]`       | `[]`        | 二维码列表(v-model) |
+| **size**              | `number`             | `150`       | 默认尺寸              |
+| **margin**            | `number`             | `10`        | 默认边距              |
+| **dotScale**          | `number`             | `0.35`      | 默认定位点缩放        |
+| **colorDark**         | `string`             | `undefined` | 默认深色              |
+| **colorLight**        | `string`             | `undefined` | 默认浅色              |
+| **correctLevel**      | `0 \| 1 \| 2 \| 3`   | `undefined` | 默认纠错等级          |
+| **logoImage**         | `string`             | `undefined` | 默认 Logo             |
+| **logoScale**         | `number`             | `undefined` | 默认 Logo 比例        |
+| **logoMargin**        | `number`             | `undefined` | 默认 Logo 边距        |
+| **logoCornerRadius**  | `number`             | `undefined` | 默认 Logo 圆角        |
+| **backgroundImage**   | `string`             | `undefined` | 默认背景图            |
+| **backgroundDimming** | `string`             | `undefined` | 默认背景遮罩          |
+| **gifBackgroundURL**  | `string`             | `undefined` | 默认 GIF 背景         |
+| **whiteMargin**       | `boolean`            | `undefined` | 默认白色边距          |
+| **autoColor**         | `boolean`            | `undefined` | 默认自动计算浅色      |
+| **maskPattern**       | `number`             | `undefined` | 默认掩码图案          |
+| **version**           | `number`             | `undefined` | 默认版本              |
+| **layout**            | `"grid" \| "list"`   | `"grid"`    | 布局方式              |
+| **columns**           | `number`             | `5`         | 网格列数              |
+| **title**             | `string \| function` | `undefined` | 标题或标题生成函数    |
+| **showItemActions**   | `boolean`            | `true`      | 显示单项操作按钮      |
+| **concurrency**       | `number`             | `50`        | 批量生成并发数        |
+| **autoGenerate**      | `boolean`            | `true`      | 自动生成开关          |
+
+**title 函数签名:**
+```typescript
+title?: (item: QRCodeItem, index: number) => string
+```
+
+**配置优先级:**
+1. `item.options` 中的配置(最高优先级)
+2. Props 中的默认配置
+3. 组件内部默认值
+
+---
+
+### Events
+
+| 事件名                    | 参数                    | 触发时机       |
+| ------------------------- | ----------------------- | -------------- |
+| **update:items**          | `(items: QRCodeItem[])` | 列表变化时     |
+| **add**                   | `(item: QRCodeItem)`    | 添加二维码     |
+| **remove**                | `(id: string)`          | 删除二维码     |
+| **generate**              | `(item: QRCodeItem)`    | 生成单个二维码 |
+| **clear**                 | `(id: string)`          | 清除二维码     |
+| **generate-all-complete** | `({ success, fail })`   | ✅ 批量生成完成 |
+| **download-all-complete** | `(count: number)`       | ✅ 批量下载完成 |
+| **print-all-complete**    | `(count: number)`       | ✅ 批量打印完成 |
+
+---
+
+### Exposed Methods
+
+```typescript
+interface VbQrMultiExposed {
+  // 数据
+  qrList: Ref<QRCodeItem[]>
+  
+  // 管理方法
+  addQRCode(item?: Partial<QRCodeItem>): void
+  removeQRCode(id: string): void
+  clearAll(): void
+  
+  // 单个操作
+  generateOne(item: QRCodeItem): void
+  downloadOne(item: QRCodeItem, index: number): void
+  printOne(item: QRCodeItem, index: number): void
+  
+  // 批量操作 - 返回 Promise
+  generateAll(): Promise<{ success: number; fail: number }>
+  downloadAll(): Promise<void>
+  printAll(): Promise<void>
+  
+  // 获取子组件引用
+  getQRRef(id: string): InstanceType<typeof VbQrGen> | undefined
+}
+```
+
+#### addQRCode(item?)
+
+添加新二维码。
+
+**参数:**
+```typescript
+{
+  id?: string      // 可选,自动生成
+  text?: string    // 文本内容
+  title?: string   // 标题
+  options?: QRCodeGenerateOptions  // 自定义配置
+}
+```
+
+**示例:**
+```typescript
+multiRef.value?.addQRCode({
+  text: 'https://example.com',
+  title: '新产品',
+  options: {
+    size: 200,
+    colorDark: '#1677ff'
+  }
+})
+```
+
+---
+
+#### removeQRCode(id)
+
+删除指定二维码。
+
+**参数:**
+- `id`: 二维码 ID
+
+**示例:**
+```typescript
+multiRef.value?.removeQRCode('qr_123')
+```
+
+---
+
+#### generateOne(item)
+
+生成单个二维码。
+
+**参数:**
+- `item`: QRCodeItem 对象
+
+**示例:**
+```typescript
+const item = qrItems.value[0]
+multiRef.value?.generateOne(item)
+```
+
+---
+
+#### downloadOne(item, index)
+
+下载单个二维码。
+
+**参数:**
+- `item`: QRCodeItem 对象
+- `index`: 索引(用于生成文件名)
+
+**示例:**
+```typescript
+multiRef.value?.downloadOne(qrItems.value[0], 0)
+```
+
+---
+
+#### printOne(item, index)
+
+打印单个二维码。
+
+**参数:**
+- `item`: QRCodeItem 对象
+- `index`: 索引(用于生成标题)
+
+**示例:**
+```typescript
+multiRef.value?.printOne(qrItems.value[0], 0)
+```
+
+---
+
+#### generateAll()
+
+批量生成所有二维码(并发处理)。
+
+**返回值:** `Promise<{ success: number; fail: number }>`
+
+**示例:**
+```typescript
+multiRef.value?.generateAll()
+  .then((result) => {
+    console.log(`成功: ${result.success}, 失败: ${result.fail}`)
+  })
+  .catch((error) => {
+    console.error('批量生成出错', error)
+  })
+```
+
+**性能参考(100个二维码):**
+- 并发 50:约 5-10 秒
+- 并发 100:约 3-5 秒
+- 并发 200:约 2-3 秒
+
+---
+
+#### downloadAll()
+
+批量下载所有二维码(合并为一张图片)。
+
+**返回值:** `Promise<void>`
+
+**示例:**
+```typescript
+multiRef.value?.downloadAll()
+  .then(() => console.log('批量下载完成'))
+  .catch((error) => console.error('下载失败', error))
+```
+
+---
+
+#### printAll()
+
+批量打印所有二维码(合并在一个页面)。
+
+**返回值:** `Promise<void>`
+
+**示例:**
+```typescript
+multiRef.value?.printAll()
+  .then(() => console.log('批量打印完成'))
+  .catch((error) => console.error('打印失败', error))
+```
+
+---
+
+#### clearAll()
+
+清空所有二维码(不清除列表,只清除生成的图片)。
+
+**示例:**
+```typescript
+multiRef.value?.clearAll()
+```
+
+---
+
+#### getQRRef(id)
+
+获取指定二维码的子组件引用。
+
+**参数:**
+- `id`: 二维码 ID
+
+**返回值:** `InstanceType<typeof VbQrGen> | undefined`
+
+**示例:**
+```typescript
+const qrRef = multiRef.value?.getQRRef('qr_123')
+if (qrRef) {
+  qrRef.downloadQRCode('custom-name')
+}
+```
+
+---
+
+### Slots
+
+| 插槽名    | 作用域参数                                                  | 说明             |
+| --------- | ----------------------------------------------------------- | ---------------- |
+| **item**  | `{ item, index, title, generate, download, print, remove }` | 自定义二维码卡片 |
+| **empty** | -                                                           | 自定义空状态     |
+
+**item 插槽参数详解:**
+```typescript
+{
+  item: QRCodeItem          // 当前项数据
+  index: number             // 索引
+  title: string             // 计算后的标题
+  generate: () => void      // 生成函数
+  download: () => void      // 下载函数
+  print: () => void         // 打印函数
+  remove: () => void        // 删除函数
+}
+```
+
+**示例:**
+```vue
+<VbQrMulti v-model:items="items">
+  <template #item="{ item, index, title, generate, download, print, remove }">
+    <div class="custom-card">
+      <div class="card-header">
+        <h3>{{ title }}</h3>
+        <el-button size="small" @click="remove">删除</el-button>
+      </div>
+      
+      <VbQrGen
+        :ref="(el) => setQRRef(item.id, el)"
+        v-model="item.text"
+        :title="title"
+      />
+      
+      <div class="card-footer">
+        <el-button size="small" @click="generate">生成</el-button>
+        <el-button size="small" @click="download">下载</el-button>
+        <el-button size="small" @click="print">打印</el-button>
+      </div>
+    </div>
+  </template>
+  
+  <template #empty>
+    <div class="empty-state">
+      <p>暂无二维码</p>
+      <el-button @click="addFirst">添加第一个</el-button>
+    </div>
+  </template>
+</VbQrMulti>
+```
+
+---
+
+## 🎯 常见场景
+
+### 1. 模态框中使用
+
+```vue
+<template>
+  <el-dialog v-model="visible" title="二维码">
+    <VbQrGen
+      ref="qrRef"
+      :model-value="currentURL"
+      :size="300"
+    />
+    <template #footer>
+      <el-button @click="download">下载</el-button>
+      <el-button @click="print">打印</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue'
+
+const visible = ref(false)
+const currentURL = ref('')
+const qrRef = ref()
+
+// 对话框打开时生成
+watch(visible, (val) => {
+  if (val && currentURL.value) {
+    setTimeout(() => {
+      qrRef.value?.generateQRCode()
+    }, 100)
+  }
+})
+
+function download() {
+  qrRef.value?.downloadQRCode('qrcode')
+}
+
+function print() {
+  qrRef.value?.printQRCode('二维码')
+}
+</script>
+```
+
+---
+
+### 2. 带 Logo 的二维码
+
+```vue
+<VbQrGen
+  v-model="text"
+  logo-image="/logo.png"
+  :logo-scale="0.3"
+  :logo-margin="8"
+  :logo-corner-radius="10"
+/>
+```
+
+---
+
+### 3. 动态标题
+
+```vue
+<VbQrMulti
+  v-model:items="items"
+  :title="(item, index) => {
+    // 优先使用 item.title
+    if (item.title) return item.title
+    
+    // 否则从 URL 提取域名
+    try {
+      const url = new URL(item.text)
+      return url.hostname
+    } catch {
+      return `二维码 ${index + 1}`
+    }
+  }}"
+/>
+```
+
+---
+
+### 4. 大量数据优化
+
+```vue
+<template>
+  <!-- 关闭自动生成,手动控制 -->
+  <VbQrMulti
+    ref="multiRef"
+    v-model:items="items"
+    :auto-generate="false"
+    :concurrency="30"
+  />
+  <el-button @click="manualGenerate">手动生成</el-button>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+
+const multiRef = ref()
+const items = ref([...])  // 大量数据
+
+onMounted(() => {
+  // 等待 DOM 渲染后手动生成
+  setTimeout(() => {
+    multiRef.value?.generateAll()
+  }, 100)
+})
+
+function manualGenerate() {
+  multiRef.value?.generateAll()
+    .then((result) => {
+      console.log(`生成完成: ${result.success}/${result.fail}`)
+    })
+}
+</script>
+```
+
+---
+
+### 5. 导出为 PDF
+
+```vue
+<script setup lang="ts">
+import jsPDF from 'jspdf'
+
+async function exportToPDF() {
+  // 先生成所有二维码
+  await multiRef.value?.generateAll()
+  
+  const pdf = new jsPDF()
+  let y = 20
+  
+  // 遍历所有二维码
+  for (let i = 0; i < items.value.length; i++) {
+    const qrRef = multiRef.value?.getQRRef(items.value[i].id)
+    const dataURL = qrRef?.getQRCodeDataURL()
+    
+    if (dataURL) {
+      pdf.addImage(dataURL, 'PNG', 20, y, 50, 50)
+      pdf.text(items.value[i].title || `二维码 ${i + 1}`, 80, y + 25)
+      y += 60
+      
+      if (y > 250) {
+        pdf.addPage()
+        y = 20
+      }
+    }
+  }
+  
+  pdf.save('qrcodes.pdf')
+}
+</script>
+```
+
+---
+
+## ⚠️ 注意事项
+
+### 异步处理
+
+✅ **推荐** - 使用 Promise 链:
+```typescript
+qrRef.value?.generateQRCode()
+  .then((dataURL) => console.log('成功', dataURL))
+  .catch((error) => console.error('失败', error))
+```
+
+⚠️ **可用但不推荐** - async/await:
+```typescript
+async function handleGenerate() {
+  try {
+    const dataURL = await qrRef.value?.generateQRCode()
+    console.log('成功', dataURL)
+  } catch (error) {
+    console.error('失败', error)
+  }
+}
+```
+
+---
+
+### 状态访问
+
+❌ **错误** - 直接访问内部状态:
+```typescript
+const url = qrRef.value?.qrcodeURL  // ❌ 不存在
+const loading = qrRef.value?.isLoading  // ❌ 不存在
+```
+
+✅ **正确** - 使用 getter 方法:
+```typescript
+const state = qrRef.value?.getQRCodeState()
+const url = state?.url
+const loading = state?.loading
+```
+
+---
+
+### 性能建议
+
+1. **大量二维码时关闭自动生成**
+   ```vue
+   <VbQrMulti :auto-generate="false" />
+   ```
+
+2. **调整并发数**
+   ```vue
+   <!-- 根据机器性能调整 -->
+   <VbQrMulti :concurrency="30" />
+   ```
+
+3. **降低二维码尺寸**
+   ```vue
+   <VbQrMulti :size="100" />
+   ```
+
+4. **使用列表布局**
+   ```vue
+   <VbQrMulti layout="list" />
+   ```
+
+---
+
+## 🔧 故障排查
+
+### Q1: 生成失败
+
+**可能原因:**
+- text 为空
+- 文本过长超出容量
+- 网络问题(在线资源)
+
+**解决:**
+```typescript
+qrRef.value?.generateQRCode()
+  .catch((error) => {
+    console.error('错误:', error)
+    // 检查 text
+    // 尝试降低 correctLevel
+    // 移除 logoImage 测试
+  })
+```
+
+---
+
+### Q2: 批量生成部分失败
+
+**可能原因:**
+- 并发数过高
+- 某些文本无效
+
+**解决:**
+```vue
+<VbQrMulti 
+  :concurrency="20"
+  @generate-all-complete="handleComplete"
+/>
+
+<script setup>
+function handleComplete(result) {
+  console.log(`成功: ${result.success}, 失败: ${result.fail}`)
+}
+</script>
+```
+
+---
+
+### Q3: 打印窗口被拦截
+
+**原因:** 浏览器弹窗拦截
+
+**解决:** 确保用户主动触发(点击按钮),避免在异步回调中调用
+
+---
+
+## 📚 相关文档
+
+- [README.md](./README.md) - 快速开始指南
+- [OPTIMIZATION_SUMMARY.md](./OPTIMIZATION_SUMMARY.md) - 优化总结
+- [MIGRATION_GUIDE.md](./MIGRATION_GUIDE.md) - 迁移指南
+
+---
+
+**最后更新**: 2026-04-13

+ 246 - 0
UI/VAP_V3.VUE/src/components/qrcode/README.md

@@ -0,0 +1,246 @@
+# 二维码组件使用文档
+
+## 快速开始
+
+### VbQrGen - 单个二维码组件
+
+```vue
+<template>
+  <VbQrGen 
+    ref="qrRef"
+    v-model="text"
+    title="我的二维码"
+    :size="200"
+  />
+  <el-button @click="generate">生成</el-button>
+  <el-button @click="download">下载</el-button>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import VbQrGen from '@/components/qrcode/VbQrGen.vue'
+
+const qrRef = ref<InstanceType<typeof VbQrGen>>()
+const text = ref('https://example.com')
+
+function generate() {
+  qrRef.value?.generateQRCode()
+}
+
+function download() {
+  qrRef.value?.downloadQRCode('my-qr')
+}
+</script>
+```
+
+---
+
+### VbQrMulti - 批量二维码组件
+
+```vue
+<template>
+  <VbQrMulti
+    ref="multiRef"
+    v-model:items="qrItems"
+    :columns="4"
+  />
+  <el-button @click="generateAll">批量生成</el-button>
+  <el-button @click="downloadAll">批量下载</el-button>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import VbQrMulti from '@/components/qrcode/VbQrMulti.vue'
+import type { QRCodeItem } from '@/components/qrcode/VbQrMulti.vue'
+
+const multiRef = ref<InstanceType<typeof VbQrMulti>>()
+const qrItems = ref<QRCodeItem[]>([
+  { id: '1', text: 'https://a.com', title: '产品 A' },
+  { id: '2', text: 'https://b.com', title: '产品 B' }
+])
+
+function generateAll() {
+  multiRef.value?.generateAll()
+    .then((result) => {
+      console.log(`成功: ${result.success}, 失败: ${result.fail}`)
+    })
+}
+
+function downloadAll() {
+  multiRef.value?.downloadAll()
+}
+</script>
+```
+
+---
+
+## 核心 API
+
+### VbQrGen 方法
+
+| 方法                        | 说明          | 返回值                    |
+| --------------------------- | ------------- | ------------------------- |
+| `generateQRCode(options?)`  | 生成二维码    | `Promise<dataURL>`        |
+| `clearQRCode()`             | 清除二维码    | `void`                    |
+| `downloadQRCode(fileName?)` | 下载二维码    | `Promise<void>`           |
+| `printQRCode(title?)`       | 打印二维码    | `Promise<void>`           |
+| `getQRCodeDataURL()`        | 获取 Data URL | `string \| null`          |
+| `getQRCodeState()`          | 获取状态      | `{ url, loading, error }` |
+
+### VbQrMulti 方法
+
+| 方法                       | 说明       | 返回值                     |
+| -------------------------- | ---------- | -------------------------- |
+| `addQRCode(item?)`         | 添加二维码 | `void`                     |
+| `removeQRCode(id)`         | 删除二维码 | `void`                     |
+| `generateOne(item)`        | 生成单个   | `void`                     |
+| `downloadOne(item, index)` | 下载单个   | `void`                     |
+| `printOne(item, index)`    | 打印单个   | `void`                     |
+| `generateAll()`            | 批量生成   | `Promise<{success, fail}>` |
+| `downloadAll()`            | 批量下载   | `Promise<void>`            |
+| `printAll()`               | 批量打印   | `Promise<void>`            |
+| `clearAll()`               | 清空全部   | `void`                     |
+
+---
+
+## 常用配置
+
+### VbQrGen Props
+
+```vue
+<VbQrGen
+  v-model="text"
+  :size="200"              <!-- 尺寸 -->
+  :margin="10"             <!-- 边距 -->
+  :dot-scale="0.35"        <!-- 定位点大小 (0-1) -->
+  color-dark="#000000"     <!-- 深色块颜色 -->
+  color-light="#ffffff"    <!-- 浅色块颜色 -->
+  :correct-level="1"       <!-- 纠错等级 (0-3) -->
+  logo-image="/logo.png"   <!-- Logo 图片 -->
+  :logo-scale="0.3"        <!-- Logo 比例 -->
+/>
+```
+
+### VbQrMulti Props
+
+```vue
+<VbQrMulti
+  v-model:items="items"
+  :layout="'grid'"         <!-- 布局: grid/list -->
+  :columns="5"             <!-- 网格列数 -->
+  :size="150"              <!-- 默认尺寸 -->
+  :dot-scale="0.35"        <!-- 默认定位点大小 -->
+  :concurrency="50"        <!-- 并发数 -->
+  :auto-generate="true"    <!-- 自动生成 -->
+  :show-item-actions="true"<!-- 显示操作按钮 -->
+/>
+```
+
+---
+
+## 事件监听
+
+### VbQrGen Events
+
+```vue
+<VbQrGen
+  @success="(dataURL) => console.log('生成成功', dataURL)"
+  @error="(error) => console.error('生成失败', error)"
+/>
+```
+
+### VbQrMulti Events
+
+```vue
+<VbQrMulti
+  @add="(item) => console.log('添加', item)"
+  @remove="(id) => console.log('删除', id)"
+  @generate-all-complete="(result) => {
+    console.log(`成功: ${result.success}, 失败: ${result.fail}`)
+  }"
+  @download-all-complete="(count) => console.log(`下载 ${count} 个`)"
+  @print-all-complete="(count) => console.log(`打印 ${count} 个`)"
+/>
+```
+
+---
+
+## 高级用法
+
+### 自定义配置生成
+
+```typescript
+// VbQrGen - 带自定义配置
+qrRef.value?.generateQRCode({
+  text: 'https://example.com',
+  size: 300,
+  colorDark: '#1677ff',
+  dotScale: 0.5
+})
+```
+
+### 动态标题
+
+```vue
+<!-- VbQrMulti - 动态生成标题 -->
+<VbQrMulti
+  :title="(item, index) => item.title || `二维码 ${index + 1}`"
+/>
+```
+
+### 自定义卡片
+
+```vue
+<VbQrMulti v-model:items="items">
+  <template #item="{ item, title, generate, download, print, remove }">
+    <div class="custom-card">
+      <h3>{{ title }}</h3>
+      <VbQrGen :ref="..." v-model="item.text" />
+      <el-button @click="generate">生成</el-button>
+      <el-button @click="download">下载</el-button>
+      <el-button @click="remove">删除</el-button>
+    </div>
+  </template>
+</VbQrMulti>
+```
+
+### 关闭自动生成
+
+```vue
+<!-- 大量数据时建议关闭,手动控制 -->
+<VbQrMulti
+  :auto-generate="false"
+  ref="multiRef"
+/>
+
+<script setup>
+onMounted(() => {
+  setTimeout(() => {
+    multiRef.value?.generateAll()
+  }, 100)
+})
+</script>
+```
+
+---
+
+## 注意事项
+
+1. **异步处理**:所有方法返回 Promise,使用 `.then().catch()` 或 `await`
+2. **状态访问**:使用 `getQRCodeState()` 而非直接访问内部状态
+3. **并发控制**:VbQrMulti 默认并发 50,可根据性能调整
+4. **自动生成**:默认开启,大量数据时建议关闭
+5. **错误处理**:批量操作中单个失败不影响其他继续执行
+
+---
+
+## 完整示例
+
+查看测试文件获取更多示例:
+- [VbQrGen 测试](../../views/test/index.vue)
+- [VbQrMulti 测试](../../views/test/index1.vue)
+
+详细文档:
+- [API 参考](./API_REFERENCE.md)
+- [优化总结](./OPTIMIZATION_SUMMARY.md)
+- [迁移指南](./MIGRATION_GUIDE.md)

+ 430 - 0
UI/VAP_V3.VUE/src/components/qrcode/VbQrGen.vue

@@ -0,0 +1,430 @@
+<script setup lang="ts">
+import { useQRCode } from "vue3-next-qrcode"
+import "vue3-next-qrcode/es/style.css"
+
+// 二维码生成选项类型
+export interface QRCodeGenerateOptions {
+	text?: string // 二维码文本内容
+	size?: number // 二维码尺寸(像素)
+	margin?: number // 二维码边距(像素)
+	correctLevel?: 0 | 1 | 2 | 3 // 纠错等级 (0=L, 1=M, 2=Q, 3=H)
+	maskPattern?: number // 掩码图案 (0-7),用于优化二维码图案分布
+	version?: number // 二维码版本 (1-40),控制二维码容量和复杂度
+	colorDark?: string // 二维码深色块颜色
+	colorLight?: string // 二维码浅色块颜色
+	autoColor?: boolean // 是否自动计算浅色值,根据背景智能调整
+	logoImage?: string // 中心Logo图片地址
+	logoScale?: number // Logo尺寸相对于二维码的比例 (0-1)
+	logoMargin?: number // Logo边距(像素)
+	logoCornerRadius?: number // Logo圆角半径(像素)
+	backgroundImage?: string // 背景图片地址
+	backgroundDimming?: string // 背景图片遮罩颜色
+	gifBackgroundURL?: string // GIF背景图片地址
+	whiteMargin?: boolean // 是否使用白色边距,避免透明背景
+	dotScale?: number // 点的缩放比例 (0-1),默认 1,越小定位点越精致
+}
+
+const props = withDefaults(
+	defineProps<{
+		modelValue?: string // 二维码文本内容
+		title?: string // 标题文本
+		titlePosition?: "top" | "bottom" // 标题位置:上方或下方
+		titleStyle?: object // 标题自定义样式
+		size?: number // 二维码尺寸(像素)
+		margin?: number // 二维码边距(像素)
+		colorDark?: string // 二维码深色块颜色
+		colorLight?: string // 二维码浅色块颜色
+		correctLevel?: 0 | 1 | 2 | 3 // 纠错等级 (0=L, 1=M, 2=Q, 3=H)
+		logoImage?: string // 中心Logo图片地址
+		logoScale?: number // Logo尺寸相对于二维码的比例
+		logoMargin?: number // Logo边距(像素)
+		logoCornerRadius?: number // Logo圆角半径(像素)
+		backgroundImage?: string // 背景图片地址
+		backgroundDimming?: string // 背景图片遮罩颜色
+		gifBackgroundURL?: string // GIF背景图片地址
+		whiteMargin?: boolean // 是否使用白色边距
+		autoColor?: boolean // 是否自动计算浅色值
+		dotScale?: number // 点的缩放比例 (0-1),默认 1,越小定位点越小
+		loadingText?: string // 加载提示文本
+		errorText?: string // 错误提示文本
+	}>(),
+	{
+		modelValue: "",
+		title: "",
+		titlePosition: "top",
+		titleStyle: undefined,
+		size: 200,
+		margin: 12,
+		colorDark: "#000000",
+		colorLight: "#ffffff",
+		correctLevel: 1,
+		logoImage: undefined,
+		logoScale: 0.4,
+		logoMargin: 6,
+		logoCornerRadius: 8,
+		backgroundImage: undefined,
+		backgroundDimming: "rgba(0, 0, 0, 0)",
+		gifBackgroundURL: undefined,
+		whiteMargin: true,
+		autoColor: true,
+		dotScale: 1, // 默认减小定位点大小
+		loadingText: "生成中...",
+		errorText: "生成失败"
+	}
+)
+
+const emits = defineEmits<{
+	(e: "update:modelValue", v: string): void
+	(e: "success", dataURL: ArrayBuffer | string | undefined): void
+	(e: "error", error: unknown): void
+}>()
+
+// 使用 useQRCode Composable
+const { qrcodeURL, isLoading, error: qrError, generate, clear } = useQRCode()
+
+/**
+ * 生成二维码(暴露给父组件调用)
+ * @param options 二维码配置选项
+ * @returns Promise,成功时解析为 dataURL
+ */
+function generateQRCode(
+	options?: QRCodeGenerateOptions
+): Promise<ArrayBuffer | string | undefined> {
+	// 如果传入了 text 则使用传入的,否则使用 modelValue
+	const textToUse = options?.text !== undefined ? options.text : props.modelValue
+
+	if (!textToUse) {
+		console.warn("VbQrGen:未提供二维码文本内容")
+		const error = new Error("未提供二维码文本内容")
+		emits("error", error)
+		return Promise.reject(error)
+	}
+
+	return generate({
+		text: textToUse,
+		size: options?.size ?? props.size,
+		margin: options?.margin ?? props.margin,
+		correctLevel: options?.correctLevel ?? props.correctLevel,
+		maskPattern: options?.maskPattern,
+		version: options?.version,
+		colorDark: options?.colorDark ?? props.colorDark,
+		colorLight: options?.colorLight ?? props.colorLight,
+		autoColor: options?.autoColor ?? props.autoColor,
+		logoImage: options?.logoImage ?? props.logoImage,
+		logoScale: options?.logoScale ?? props.logoScale,
+		logoMargin: options?.logoMargin ?? props.logoMargin,
+		logoCornerRadius: options?.logoCornerRadius ?? props.logoCornerRadius,
+		backgroundImage: options?.backgroundImage ?? props.backgroundImage,
+		backgroundDimming: options?.backgroundDimming ?? props.backgroundDimming,
+		gifBackgroundURL: options?.gifBackgroundURL ?? props.gifBackgroundURL,
+		whiteMargin: options?.whiteMargin ?? props.whiteMargin,
+		dotScale: options?.dotScale ?? props.dotScale
+	})
+		.then((dataURL) => {
+			// 将返回值转换为 emits 期望的类型
+			const result = dataURL as ArrayBuffer | string | undefined
+			emits("success", result)
+			return result
+		})
+		.catch((error) => {
+			console.error("VbQrGen:二维码生成失败", error)
+			emits("error", error)
+			throw error
+		})
+}
+
+/**
+ * 清除二维码
+ */
+function clearQRCode() {
+	clear()
+}
+
+/**
+ * 下载二维码图片
+ * @param fileName 文件名(不含扩展名),默认为 'qrcode'
+ * @returns Promise
+ */
+function downloadQRCode(fileName: string = "qrcode"): Promise<void> {
+	return new Promise((resolve, reject) => {
+		try {
+			const dataURL = qrcodeURL.value
+			if (!dataURL || typeof dataURL !== "string") {
+				const error = new Error("VbQrGen:二维码数据未生成")
+				console.error(error.message)
+				reject(error)
+				return
+			}
+
+			// 创建下载链接
+			const link = document.createElement("a")
+			link.href = dataURL
+			link.download = `${fileName}.png`
+			document.body.appendChild(link)
+			link.click()
+			document.body.removeChild(link)
+
+			console.log("VbQrGen:二维码下载成功")
+			resolve()
+		} catch (error) {
+			console.error("VbQrGen:二维码下载失败", error)
+			reject(error)
+		}
+	})
+}
+
+/**
+ * 打印二维码图片
+ * @param title 打印标题,默认为使用组件的 title prop 或 '二维码'
+ * @returns Promise
+ */
+function printQRCode(title?: string): Promise<void> {
+	return new Promise((resolve, reject) => {
+		try {
+			// 使用 Composable 生成的 qrcodeURL
+			const dataURL = qrcodeURL.value
+			if (!dataURL || typeof dataURL !== "string") {
+				const error = new Error("VbQrGen:二维码数据未生成")
+				console.error(error.message)
+				reject(error)
+				return
+			}
+
+			// 确定打印标题:优先使用传入的 title,其次使用 props.title,最后使用默认值
+			const printTitle = title || props.title || "二维码"
+
+			// 尝试获取自定义插槽内容用于打印(优先使用 ref,降级为 querySelector)
+			const contentContainer = contentRef.value || document.querySelector(".vb-qr-content")
+			let customContentHTML = ""
+
+			if (contentContainer) {
+				// 克隆节点以获取当前显示的内容(包括自定义插槽)
+				const clonedContent = contentContainer.cloneNode(true) as HTMLElement
+				customContentHTML = clonedContent.innerHTML
+			}
+
+			// 创建打印窗口
+			const printWindow = window.open("", "_blank")
+			if (!printWindow) {
+				const error = new Error("VbQrGen:无法打开打印窗口,请检查浏览器弹窗设置")
+				console.error(error.message)
+				reject(error)
+				return
+			}
+
+			// 构建打印页面内容(不使用 script 标签,改用 onload 属性)
+			const printContent = `
+				<!DOCTYPE html>
+				<html>
+				<head>
+					<meta charset="UTF-8">
+					<title>${printTitle}</title>
+					<style>
+						* {
+							margin: 0;
+							padding: 0;
+							box-sizing: border-box;
+						}
+						body {
+							display: flex;
+							flex-direction: column;
+							justify-content: center;
+							align-items: center;
+							min-height: 100vh;
+							padding: 20px;
+							font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+						}
+						.title {
+							font-size: 24px;
+							font-weight: bold;
+							margin-bottom: 20px;
+							color: #333;
+							text-align: center;
+						}
+						.qrcode-container {
+							display: flex;
+							justify-content: center;
+							align-items: center;
+							margin: 20px 0;
+						}
+						.qrcode-image {
+							max-width: 100%;
+							height: auto;
+						}
+						@media print {
+							body {
+								print-color-adjust: exact;
+								-webkit-print-color-adjust: exact;
+							}
+						}
+					</style>
+				</head>
+				<body onload="setTimeout(function() { window.print(); setTimeout(function() { window.close(); }, 100); }, 500)">
+					<div class="title">${printTitle}</div>
+					<div class="qrcode-container">
+						${customContentHTML || `<img src="${dataURL}" alt="二维码" class="qrcode-image" />`}
+					</div>
+				</body>
+				</html>
+			`
+
+			// 使用 Blob 和 URL.createObjectURL 替代已弃用的 document.write
+			const blob = new Blob([printContent], { type: "text/html;charset=utf-8" })
+			const url = URL.createObjectURL(blob)
+			printWindow.location.href = url
+
+			console.log("VbQrGen:二维码打印窗口已打开")
+			resolve()
+		} catch (error) {
+			console.error("VbQrGen:二维码打印失败", error)
+			reject(error)
+		}
+	})
+}
+
+/**
+ * 获取二维码 Data URL
+ * @returns Data URL 字符串或 null
+ */
+function getQRCodeDataURL(): string | null {
+	try {
+		const dataURL = qrcodeURL.value
+		if (!dataURL || typeof dataURL !== "string") {
+			console.warn("VbQrGen:二维码数据未生成")
+			return null
+		}
+		return dataURL
+	} catch (error) {
+		console.error("VbQrGen:获取二维码 Data URL 失败", error)
+		return null
+	}
+}
+
+// 标题样式
+const titleStyle = computed(() => {
+	const defaultStyle: Record<string, string> = {
+		fontSize: "16px",
+		fontWeight: "bold",
+		color: "#333",
+		marginBottom: props.titlePosition === "top" ? "12px" : "0",
+		marginTop: props.titlePosition === "bottom" ? "12px" : "0",
+		textAlign: "center"
+	}
+	return {
+		...defaultStyle,
+		...(props.titleStyle || {})
+	}
+})
+
+// 内容容器引用(用于打印时获取自定义插槽内容)
+const contentRef = ref<HTMLElement | null>(null)
+
+// 暴露方法给父组件
+defineExpose({
+	// 核心方法
+	generateQRCode,
+	clearQRCode,
+	downloadQRCode,
+	printQRCode,
+	getQRCodeDataURL,
+	// 状态获取方法(不直接暴露内部状态)
+	getQRCodeState() {
+		return {
+			url: qrcodeURL.value,
+			loading: isLoading.value,
+			error: qrError.value
+		}
+	}
+})
+</script>
+
+<template>
+	<div class="vb-qr-gen">
+		<!-- 标题在上方 -->
+		<template v-if="props.title && props.titlePosition === 'top'">
+			<!-- 使用插槽自定义标题 -->
+			<slot v-if="$slots.title" name="title"></slot>
+			<!-- 使用默认标题 -->
+			<div v-else class="vb-qr-title" :style="titleStyle">{{ props.title }}</div>
+		</template>
+
+		<!-- 二维码图片 -->
+		<div v-if="qrcodeURL && typeof qrcodeURL === 'string'" ref="contentRef" class="vb-qr-content">
+			<!-- 使用插槽自定义二维码显示 -->
+			<slot v-if="$slots.qrcode" name="qrcode" :qrcode-url="qrcodeURL" :size="props.size"></slot>
+			<!-- 默认显示图片 -->
+			<img
+				v-else
+				:src="qrcodeURL"
+				alt="二维码"
+				class="vb-qr-image"
+				:style="{ width: props.size + 'px', height: props.size + 'px' }" />
+		</div>
+		<!-- 加载中状态 -->
+		<div v-else-if="isLoading" class="vb-qr-content">
+			<!-- 使用插槽自定义加载状态 -->
+			<slot v-if="$slots.loading" name="loading"></slot>
+			<!-- 默认加载提示 -->
+			<template v-else>
+				<div class="vb-qr-spinner"></div>
+				<span>{{ loadingText }}</span>
+			</template>
+		</div>
+		<!-- 错误状态 -->
+		<div v-else-if="qrError" class="vb-qr-content">
+			<!-- 使用插槽自定义错误状态 -->
+			<slot v-if="$slots.error" name="error" :error="qrError"></slot>
+			<!-- 默认错误提示 -->
+			<template v-else>
+				<span>{{ errorText }}</span>
+			</template>
+		</div>
+
+		<!-- 标题在下方 -->
+		<template v-if="props.title && props.titlePosition === 'bottom'">
+			<!-- 使用插槽自定义标题 -->
+			<slot v-if="$slots.title" name="title"></slot>
+			<!-- 使用默认标题 -->
+			<div v-else class="vb-qr-title" :style="titleStyle">{{ props.title }}</div>
+		</template>
+	</div>
+</template>
+
+<style scoped lang="scss">
+.vb-qr-gen {
+	display: inline-flex;
+	flex-direction: column;
+	justify-content: center;
+	align-items: center;
+
+	.vb-qr-title {
+		width: 100%;
+	}
+
+	.vb-qr-content {
+		display: flex;
+		justify-content: center;
+		align-items: center;
+	}
+
+	.vb-qr-image {
+		display: block;
+		object-fit: contain;
+	}
+
+	.vb-qr-spinner {
+		width: 30px;
+		height: 30px;
+		border: 3px solid #e5e7eb;
+		border-top-color: #1677ff;
+		border-radius: 50%;
+		animation: spin 0.75s linear infinite;
+	}
+
+	@keyframes spin {
+		to {
+			transform: rotate(360deg);
+		}
+	}
+}
+</style>

+ 842 - 0
UI/VAP_V3.VUE/src/components/qrcode/VbQrMulti.vue

@@ -0,0 +1,842 @@
+<script setup lang="ts">
+import VbQrGen from "./VbQrGen.vue"
+import type { QRCodeGenerateOptions } from "./VbQrGen.vue"
+
+// 二维码项类型
+export interface QRCodeItem {
+	id: string
+	text: string
+	title?: string
+	options?: QRCodeGenerateOptions
+}
+
+const props = withDefaults(
+	defineProps<{
+		items?: QRCodeItem[] // 初始二维码列表
+		size?: number // 默认尺寸
+		margin?: number // 默认边距
+		dotScale?: number // 默认点缩放比例
+		colorDark?: string // 默认深色块颜色
+		colorLight?: string // 默认浅色块颜色
+		correctLevel?: 0 | 1 | 2 | 3 // 默认纠错等级
+		logoImage?: string // 默认Logo图片
+		logoScale?: number // 默认Logo比例
+		logoMargin?: number // 默认Logo边距
+		logoCornerRadius?: number // 默认Logo圆角
+		backgroundImage?: string // 默认背景图
+		backgroundDimming?: string // 默认背景遮罩
+		gifBackgroundURL?: string // 默认GIF背景
+		whiteMargin?: boolean // 默认白色边距
+		autoColor?: boolean // 默认自动计算浅色
+		maskPattern?: number // 默认掩码图案
+		version?: number // 默认版本
+		layout?: "grid" | "list" // 布局方式:网格或列表
+		columns?: number // 网格列数
+		title?: string | ((item: QRCodeItem, index: number) => string) // 标题或标题生成函数
+		showItemActions?: boolean // 是否显示每个二维码卡片的操作按钮
+		concurrency?: number // 批量生成并发数,默认20
+		autoGenerate?: boolean // 是否自动生成:true=页面初始化时自动批量生成,添加新项后也自动生成
+	}>(),
+	{
+		items: () => [],
+		size: 150,
+		margin: 10,
+		dotScale: 0.35,
+		colorDark: undefined,
+		colorLight: undefined,
+		correctLevel: undefined,
+		logoImage: undefined,
+		logoScale: undefined,
+		logoMargin: undefined,
+		logoCornerRadius: undefined,
+		backgroundImage: undefined,
+		backgroundDimming: undefined,
+		gifBackgroundURL: undefined,
+		whiteMargin: undefined,
+		autoColor: undefined,
+		maskPattern: undefined,
+		version: undefined,
+		layout: "grid",
+		columns: 5,
+		title: undefined,
+		showItemActions: true,
+		concurrency: 50,
+		autoGenerate: true
+	}
+)
+
+const emits = defineEmits<{
+	(e: "update:items", items: QRCodeItem[]): void
+	(e: "add", item: QRCodeItem): void
+	(e: "remove", id: string): void
+	(e: "generate", item: QRCodeItem): void
+	(e: "clear", id: string): void
+	(e: "generate-all-complete", result: { success: number; fail: number }): void
+	(e: "download-all-complete", count: number): void
+	(e: "print-all-complete", count: number): void
+}>()
+
+// 二维码列表
+const qrList = ref<QRCodeItem[]>(props.items || [])
+
+// VbQrGen 组件引用映射
+const qrRefs = ref<Map<string, InstanceType<typeof VbQrGen>>>(new Map())
+
+// 计算样式
+const containerStyle = computed(() => {
+	if (props.layout === "grid") {
+		return {
+			display: "grid" as const,
+			gridTemplateColumns: `repeat(${props.columns}, 1fr)`,
+			gap: "20px"
+		}
+	}
+	return {
+		display: "flex" as const,
+		flexDirection: "column" as const,
+		gap: "20px"
+	}
+})
+
+/**
+ * 获取二维码标题
+ */
+function getQRTitle(item: QRCodeItem, index: number): string {
+	// 优先使用 item 自身的 title
+	if (item.title) {
+		return item.title
+	}
+
+	// 如果 props.title 是函数,调用它
+	if (typeof props.title === "function") {
+		return props.title(item, index)
+	}
+
+	// 如果 props.title 是字符串,使用它
+	if (props.title) {
+		return props.title
+	}
+
+	// 默认标题
+	return `二维码 ${index + 1}`
+}
+
+/**
+ * 合并配置选项
+ */
+function mergeOptions(item: QRCodeItem): QRCodeGenerateOptions {
+	return {
+		size: props.size,
+		margin: props.margin,
+		dotScale: props.dotScale,
+		colorDark: props.colorDark,
+		colorLight: props.colorLight,
+		correctLevel: props.correctLevel,
+		logoImage: props.logoImage,
+		logoScale: props.logoScale,
+		logoMargin: props.logoMargin,
+		logoCornerRadius: props.logoCornerRadius,
+		backgroundImage: props.backgroundImage,
+		backgroundDimming: props.backgroundDimming,
+		gifBackgroundURL: props.gifBackgroundURL,
+		whiteMargin: props.whiteMargin,
+		autoColor: props.autoColor,
+		maskPattern: props.maskPattern,
+		version: props.version,
+		...item.options
+	}
+}
+
+/**
+ * 添加二维码
+ */
+function addQRCode(item?: Partial<QRCodeItem>) {
+	const index = qrList.value.length
+	const newItem: QRCodeItem = {
+		id: item?.id || `qr_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+		text: item?.text || "",
+		title:
+			item?.title ||
+			(typeof props.title === "function" ? props.title({} as QRCodeItem, index) : props.title) ||
+			"",
+		options: mergeOptions({ options: item?.options } as QRCodeItem)
+	}
+
+	qrList.value.push(newItem)
+	emits("update:items", qrList.value)
+	emits("add", newItem)
+	message.msgSuccess("已添加新二维码")
+
+	// 如果开启了自动生成,立即生成新添加的二维码
+	if (props.autoGenerate && newItem.text) {
+		nextTick(() => {
+			const qrRef = qrRefs.value.get(newItem.id)
+			if (qrRef) {
+				try {
+					const options = mergeOptions(newItem)
+					options.text = newItem.text
+					qrRef.generateQRCode(options)
+				} catch (error) {
+					console.error(`自动生失败:`, error)
+				}
+			}
+		})
+	}
+}
+
+/**
+ * 删除二维码
+ */
+function removeQRCode(id: string) {
+	const index = qrList.value.findIndex((item) => item.id === id)
+	if (index > -1) {
+		qrList.value.splice(index, 1)
+		qrRefs.value.delete(id)
+		emits("update:items", qrList.value)
+		emits("remove", id)
+		message.msgSuccess("已删除二维码")
+	}
+}
+
+/**
+ * 批量生成所有二维码(并发处理)
+ * @returns Promise,解析为成功和失败数量
+ */
+function generateAll(): Promise<{ success: number; fail: number }> {
+	return new Promise((resolve, reject) => {
+		const total = qrList.value.length
+		if (total === 0) {
+			message.msgWarning("没有可生成的二维码")
+			resolve({ success: 0, fail: 0 })
+			return
+		}
+
+		let successCount = 0
+		let failCount = 0
+
+		// 创建任务队列
+		type TaskItem = { item: QRCodeItem; index: number }
+		const tasks: TaskItem[] = qrList.value.map((item, index) => ({ item, index }))
+
+		// 并发控制函数 - 使用信号量模式
+		function runWithConcurrency(tasks: TaskItem[], concurrency: number) {
+			let activeCount = 0
+			let taskIndex = 0
+
+			function runNext() {
+				// 如果没有更多任务且没有活动任务,完成
+				if (taskIndex >= tasks.length && activeCount === 0) {
+					const result = { success: successCount, fail: failCount }
+					emits("generate-all-complete", result)
+					resolve(result)
+					return
+				}
+
+				// 启动新任务直到达到并发限制
+				while (activeCount < concurrency && taskIndex < tasks.length) {
+					const task = tasks[taskIndex]
+					taskIndex++
+					activeCount++
+
+					// 执行任务
+					;(() => {
+						try {
+							const qrRef = qrRefs.value.get(task.item.id)
+							if (qrRef && task.item.text) {
+								const options = mergeOptions(task.item)
+								options.text = task.item.text
+
+								let resolved = false
+								let timeoutId: any = null
+
+								const cleanup = () => {
+									if (timeoutId) {
+										clearTimeout(timeoutId)
+									}
+								}
+
+								const onSuccess = () => {
+									if (!resolved) {
+										resolved = true
+										cleanup()
+										successCount++
+									}
+								}
+
+								const onError = (_error: unknown) => {
+									if (!resolved) {
+										resolved = true
+										cleanup()
+										failCount++
+									}
+								}
+
+								// 超时处理
+								timeoutId = setTimeout(() => {
+									if (!resolved) {
+										resolved = true
+										failCount++
+									}
+								}, 10000)
+
+								// 清空旧的二维码,确保能检测到新生成的
+								qrRef.clearQRCode()
+
+								// 调用生成方法
+								qrRef.generateQRCode(options)
+
+								// 轮询检查是否生成完成
+								const checkInterval = setInterval(() => {
+									if (resolved) {
+										clearInterval(checkInterval)
+										return
+									}
+
+									// 获取状态检查是否有错误
+									const state = qrRef.getQRCodeState()
+									if (state.error) {
+										onError(state.error)
+										clearInterval(checkInterval)
+										return
+									}
+
+									// 检查是否生成了新的二维码
+									if (state.url && typeof state.url === "string") {
+										onSuccess()
+										clearInterval(checkInterval)
+									}
+								}, 50)
+							} else {
+								failCount++
+							}
+						} catch (error) {
+							failCount++
+							console.error(`二维码 ${task.item.id} 生成失败:`, error)
+						} finally {
+							// 任务完成后,减少活动计数并启动下一个任务
+							activeCount--
+							runNext()
+						}
+					})()
+				}
+			}
+
+			// 开始执行
+			runNext()
+		}
+
+		// 执行并发任务
+		runWithConcurrency(tasks, props.concurrency)
+	})
+}
+
+/**
+ * 生成单个二维码
+ */
+function generateOne(item: QRCodeItem) {
+	const qrRef = qrRefs.value.get(item.id)
+	if (qrRef && item.text) {
+		const options = mergeOptions(item)
+		options.text = item.text
+		qrRef.clearQRCode()
+		qrRef.generateQRCode(options)
+	} else {
+		message.msgWarning("二维码文本为空")
+	}
+}
+
+/**
+ * 下载单个二维码
+ */
+function downloadOne(item: QRCodeItem, index: number) {
+	const qrRef = qrRefs.value.get(item.id)
+	if (qrRef) {
+		const title = getQRTitle(item, index)
+		qrRef
+			.downloadQRCode(title)
+			.then(() => {
+				message.msgSuccess(`已下载:${title}`)
+			})
+			.catch((error) => {
+				console.error(`二维码 ${item.id} 下载失败:`, error)
+				message.msgError("下载失败")
+			})
+	}
+}
+
+/**
+ * 打印单个二维码
+ */
+function printOne(item: QRCodeItem, index: number) {
+	const qrRef = qrRefs.value.get(item.id)
+	if (qrRef) {
+		const title = getQRTitle(item, index)
+		qrRef
+			.printQRCode(title)
+			.then(() => {
+				message.msgSuccess(`已打开打印窗口:${title}`)
+			})
+			.catch((error) => {
+				console.error(`二维码 ${item.id} 打印失败:`, error)
+				message.msgError("打印失败")
+			})
+	}
+}
+
+/**
+ * 批量清除所有二维码
+ */
+function clearAll() {
+	qrList.value.forEach((item) => {
+		const qrRef = qrRefs.value.get(item.id)
+		if (qrRef) {
+			qrRef.clearQRCode()
+		}
+	})
+	emits("clear", "all")
+	message.msgSuccess("已清除所有二维码")
+}
+
+/**
+ * 批量下载所有二维码(合并为一张图片)
+ * @returns Promise
+ */
+function downloadAll(): Promise<void> {
+	return new Promise((resolve, reject) => {
+		try {
+			// 收集所有已生成的二维码 Data URL
+			const dataURLs: { title: string; dataURL: string }[] = []
+
+			for (let i = 0; i < qrList.value.length; i++) {
+				const item = qrList.value[i]
+				const qrRef = qrRefs.value.get(item.id)
+				if (qrRef) {
+					const dataURL = qrRef.getQRCodeDataURL()
+					if (dataURL) {
+						dataURLs.push({
+							title: getQRTitle(item, i),
+							dataURL
+						})
+					}
+				}
+			}
+
+			if (dataURLs.length === 0) {
+				message.msgWarning("没有可下载的二维码,请先生成")
+				resolve()
+				return
+			}
+
+			// 创建 canvas 合并所有二维码
+			const canvas = document.createElement("canvas")
+			const ctx = canvas.getContext("2d")
+			if (!ctx) {
+				throw new Error("无法创建 canvas 上下文")
+			}
+
+			// 计算画布大小
+			const qrSize = props.size
+			const padding = 40
+			const titleHeight = 30
+			const cols = Math.min(dataURLs.length, props.columns)
+			const rows = Math.ceil(dataURLs.length / cols)
+
+			canvas.width = cols * (qrSize + padding) + padding
+			canvas.height = rows * (qrSize + titleHeight + padding) + padding
+
+			// 填充白色背景
+			ctx.fillStyle = "#ffffff"
+			ctx.fillRect(0, 0, canvas.width, canvas.height)
+
+			// 绘制所有二维码
+			let loadedCount = 0
+			const totalImages = dataURLs.length
+
+			const checkComplete = () => {
+				loadedCount++
+				if (loadedCount === totalImages) {
+					// 所有图片加载完成,下载
+					const link = document.createElement("a")
+					link.href = canvas.toDataURL("image/png")
+					link.download = `qrcodes_${Date.now()}.png`
+					document.body.appendChild(link)
+					link.click()
+					document.body.removeChild(link)
+
+					emits("download-all-complete", dataURLs.length)
+					message.msgSuccess(`已下载包含 ${dataURLs.length} 个二维码的图片`)
+					resolve()
+				}
+			}
+
+			for (let i = 0; i < dataURLs.length; i++) {
+				const col = i % cols
+				const row = Math.floor(i / cols)
+				const x = col * (qrSize + padding) + padding
+				const y = row * (qrSize + titleHeight + padding) + padding
+
+				// 绘制标题
+				ctx.fillStyle = "#333333"
+				ctx.font = "bold 14px Arial"
+				ctx.textAlign = "center"
+				ctx.fillText(dataURLs[i].title, x + qrSize / 2, y + 20)
+
+				// 绘制二维码
+				const img = new Image()
+				img.onload = () => {
+					ctx.drawImage(img, x, y + titleHeight, qrSize, qrSize)
+					checkComplete()
+				}
+				img.onerror = () => {
+					console.error(`图片加载失败: ${dataURLs[i].title}`)
+					checkComplete()
+				}
+				img.src = dataURLs[i].dataURL
+			}
+		} catch (error) {
+			console.error("批量下载失败:", error)
+			message.msgError("批量下载失败")
+			reject(error)
+		}
+	})
+}
+
+/**
+ * 批量打印所有二维码(合并在一个页面上)
+ * @returns Promise
+ */
+function printAll(): Promise<void> {
+	return new Promise((resolve, reject) => {
+		try {
+			// 收集所有已生成的二维码 Data URL
+			const dataURLs: { title: string; dataURL: string }[] = []
+
+			for (let i = 0; i < qrList.value.length; i++) {
+				const item = qrList.value[i]
+				const qrRef = qrRefs.value.get(item.id)
+				if (qrRef) {
+					const dataURL = qrRef.getQRCodeDataURL()
+					if (dataURL) {
+						dataURLs.push({
+							title: getQRTitle(item, i),
+							dataURL
+						})
+					}
+				}
+			}
+
+			if (dataURLs.length === 0) {
+				message.msgWarning("没有可打印的二维码,请先生成")
+				resolve()
+				return
+			}
+
+			// 创建打印窗口
+			const printWindow = window.open("", "_blank")
+			if (!printWindow) {
+				throw new Error("无法打开打印窗口,请检查浏览器弹窗设置")
+			}
+
+			// 计算布局
+			const cols = props.columns
+
+			// 构建打印页面内容
+			const printContent = `
+				<!DOCTYPE html>
+				<html>
+				<head>
+					<meta charset="UTF-8">
+					<title>批量二维码打印</title>
+					<style>
+						* {
+							margin: 0;
+							padding: 0;
+							box-sizing: border-box;
+						}
+						body {
+							padding: 15px;
+							font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+						}
+						.qr-grid {
+							display: grid;
+							grid-template-columns: repeat(${cols}, 1fr);
+							gap: 15px;
+							max-width: 1200px;
+							margin: 0 auto;
+						}
+						.qr-item {
+							display: flex;
+							flex-direction: column;
+							align-items: center;
+							padding: 10px;
+							border: none;
+							border-radius: 8px;
+							background: #fff;
+						}
+						.qr-title {
+							font-size: 14px;
+							font-weight: bold;
+							color: #333;
+							margin-bottom: 10px;
+							text-align: center;
+							word-break: break-all;
+						}
+						.qr-image {
+							width: ${props.size}px;
+							height: ${props.size}px;
+							object-fit: contain;
+						}
+						@media print {
+							body {
+								print-color-adjust: exact;
+								-webkit-print-color-adjust: exact;
+							}
+							.qr-item {
+								break-inside: avoid;
+								page-break-inside: avoid;
+							}
+						}
+					</style>
+				</head>
+				<body onload="setTimeout(function() { window.print(); setTimeout(function() { window.close(); }, 100); }, 500)">
+					<div class="qr-grid">
+						${dataURLs
+							.map(
+								(item) => `
+							<div class="qr-item">
+								<div class="qr-title">${item.title}</div>
+								<img src="${item.dataURL}" alt="${item.title}" class="qr-image" />
+							</div>
+						`
+							)
+							.join("")}
+					</div>
+				</body>
+				</html>
+			`
+
+			// 使用 Blob 和 URL.createObjectURL
+			const blob = new Blob([printContent], { type: "text/html;charset=utf-8" })
+			const url = URL.createObjectURL(blob)
+			printWindow.location.href = url
+
+			emits("print-all-complete", dataURLs.length)
+			message.msgSuccess(`已打开打印窗口,包含 ${dataURLs.length} 个二维码`)
+			resolve()
+		} catch (error) {
+			console.error("批量打印失败:", error)
+			message.msgError("批量打印失败")
+			reject(error)
+		}
+	})
+}
+
+/**
+ * 获取组件引用
+ */
+function setQRRef(id: string, ref: InstanceType<typeof VbQrGen> | null) {
+	if (ref) {
+		qrRefs.value.set(id, ref)
+	} else {
+		qrRefs.value.delete(id)
+	}
+}
+
+// 页面挂载后,如果开启了自动生成,则自动批量生成
+onMounted(() => {
+	if (props.autoGenerate && qrList.value.length > 0) {
+		// 等待 DOM 渲染完成
+		nextTick(() => {
+			// 延迟一小段时间确保所有子组件都已挂载
+			setTimeout(() => {
+				generateAll()
+			}, 100)
+		})
+	}
+})
+
+// 监听 items 变化,如果有新项且开启了自动生成,则自动生成新项
+watch(
+	() => props.items,
+	(newItems, oldItems) => {
+		if (!props.autoGenerate) return
+
+		// 如果是初始化,不处理(由 onMounted 处理)
+		if (!oldItems || oldItems.length === 0) return
+
+		// 检测是否有新项
+		if (newItems.length > oldItems.length) {
+			// 找到新添加的项
+			const newIds = newItems.map((item) => item.id)
+			const oldIds = oldItems.map((item) => item.id)
+			const addedIds = newIds.filter((id) => !oldIds.includes(id))
+
+			// 为每个新项生成二维码
+			if (addedIds.length > 0) {
+				nextTick(() => {
+					addedIds.forEach((id) => {
+						const item = newItems.find((i) => i.id === id)
+						if (item && item.text) {
+							const qrRef = qrRefs.value.get(id)
+							if (qrRef) {
+								try {
+									const options = mergeOptions(item)
+									options.text = item.text
+									qrRef.generateQRCode(options)
+								} catch (error) {
+									console.error(`自动生成失败:`, error)
+								}
+							}
+						}
+					})
+				})
+			}
+		}
+	},
+	{ deep: true }
+)
+
+// 暴露方法给父组件
+defineExpose({
+	qrList,
+	addQRCode,
+	removeQRCode,
+	generateAll,
+	generateOne,
+	downloadOne,
+	printOne,
+	clearAll,
+	downloadAll,
+	printAll,
+	getQRRef: (id: string) => qrRefs.value.get(id)
+})
+</script>
+
+<template>
+	<div class="vb-qr-multi">
+		<!-- 二维码列表/网格 -->
+		<div class="vb-qr-multi-container" :style="containerStyle">
+			<div v-for="(item, index) in qrList" :key="item.id" class="vb-qr-multi-item">
+				<!-- 使用插槽自定义二维码卡片内容 -->
+				<slot
+					name="item"
+					:item="item"
+					:index="index"
+					:title="getQRTitle(item, index)"
+					:generate="() => generateOne(item)"
+					:download="() => downloadOne(item, index)"
+					:print="() => printOne(item, index)"
+					:remove="() => removeQRCode(item.id)">
+					<!-- 默认卡片内容 -->
+					<!-- 二维码组件 -->
+					<VbQrGen
+						:ref="(el) => setQRRef(item.id, el as InstanceType<typeof VbQrGen>)"
+						v-model="item.text"
+						:title="getQRTitle(item, index)"
+						:size="item.options?.size ?? size"
+						:margin="item.options?.margin ?? margin"
+						:dot-scale="item.options?.dotScale ?? dotScale"
+						:color-dark="item.options?.colorDark"
+						:color-light="item.options?.colorLight"
+						:correct-level="item.options?.correctLevel"
+						:logo-image="item.options?.logoImage"
+						:background-image="item.options?.backgroundImage"
+						class="vb-qr-multi-qrcode" />
+
+					<!-- 二维码卡片底部操作 -->
+					<div v-if="showItemActions" class="vb-qr-multi-item-footer">
+						<!-- <el-button size="small" @click="generateOne(item)">生成</el-button> -->
+						<el-button size="small" @click="downloadOne(item, index)">下载</el-button>
+						<el-button size="small" @click="printOne(item, index)">打印</el-button>
+					</div>
+				</slot>
+			</div>
+
+			<!-- 空状态 -->
+			<div v-if="qrList.length === 0" class="vb-qr-multi-empty">
+				<slot name="empty">
+					<el-empty description="暂无二维码,点击「添加二维码」开始创建" />
+				</slot>
+			</div>
+		</div>
+	</div>
+</template>
+
+<style scoped lang="scss">
+.vb-qr-multi {
+	width: 100%;
+
+	.vb-qr-multi-toolbar {
+		margin-bottom: 20px;
+		padding: 16px;
+		background-color: #f5f7fa;
+		border-radius: 8px;
+
+		.vb-qr-multi-actions {
+			display: flex;
+			gap: 10px;
+			flex-wrap: wrap;
+		}
+	}
+
+	.vb-qr-multi-container {
+		.vb-qr-multi-item {
+			position: relative;
+			padding: 16px;
+			background-color: #fff;
+			border: 1px solid #e4e7ed;
+			border-radius: 8px;
+			transition: all 0.3s ease;
+
+			&:hover {
+				box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+				border-color: #1677ff;
+			}
+
+			.vb-qr-multi-item-header {
+				display: flex;
+				justify-content: space-between;
+				align-items: center;
+				margin-bottom: 12px;
+				padding-bottom: 8px;
+				border-bottom: 1px solid #f0f0f0;
+
+				.vb-qr-multi-item-title {
+					font-size: 14px;
+					font-weight: 600;
+					color: #303133;
+					overflow: hidden;
+					text-overflow: ellipsis;
+					white-space: nowrap;
+					max-width: calc(100% - 40px);
+				}
+
+				.vb-qr-multi-item-remove {
+					flex-shrink: 0;
+				}
+			}
+
+			.vb-qr-multi-qrcode {
+				display: flex;
+				justify-content: center;
+				align-items: center;
+				min-height: 200px;
+			}
+
+			.vb-qr-multi-item-footer {
+				display: flex;
+				justify-content: center;
+				gap: 8px;
+				margin-top: 12px;
+				padding-top: 12px;
+				border-top: 1px solid #f0f0f0;
+			}
+		}
+
+		.vb-qr-multi-empty {
+			grid-column: 1 / -1;
+			padding: 60px 20px;
+		}
+	}
+}
+</style>