| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430 |
- <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>
|