UNPKG

@wtechtec/qr-generator-core

Version:

A powerful QR code generator with customizable styling options

524 lines (519 loc) 18.8 kB
'use strict'; var QRCodeStyling = require('qr-code-styling'); var html2canvas = require('html2canvas'); // 默认配置 const defaultConfig = { text: 'Hello World', width: 800, height: 600, qrOptions: { typeNumber: 0, mode: 'Byte', errorCorrectionLevel: 'M', }, imageOptions: { hideBackgroundDots: true, imageSize: 0.4, margin: 4, crossOrigin: 'anonymous', }, dotsOptions: { color: '#0000ff', type: 'square', }, backgroundOptions: { color: '#ffffff', }, cornersSquareOptions: { color: '#0000ff', type: 'square', }, cornersDotOptions: { color: '#000000', type: 'square', }, exportOptions: { format: 'png', quality: 0.9, borderRadius: 0, }, texts: [], backgrounds: [], htmlModules: [], }; // 计算默认二维码位置和尺寸的辅助函数 const calculateDefaultQRSettings = (canvasWidth, canvasHeight, qrRatio = 0.6) => { const qrSize = Math.min(canvasWidth, canvasHeight) * qrRatio; return { qrPosition: { x: (canvasWidth - qrSize) / 2, y: (canvasHeight - qrSize) / 2 }, qrSize: { width: qrSize, height: qrSize } }; }; // 类型导出 // export type { PlatformQRGeneratorConfig }; // 主要的QR生成器类 class QRGenerator { constructor(config) { this.container = null; this.qrCode = null; this.isRendered = false; // 合并配置,如果没有提供qrPosition和qrSize,则自动计算 const mergedConfig = { ...defaultConfig, ...config }; // 如果没有明确设置二维码位置和尺寸,则根据画布尺寸自动计算 if (!config.qrPosition || !config.qrSize) { const autoSettings = calculateDefaultQRSettings(mergedConfig.width, mergedConfig.height); if (!config.qrPosition) { mergedConfig.qrPosition = autoSettings.qrPosition; } if (!config.qrSize) { mergedConfig.qrSize = autoSettings.qrSize; } } this.config = mergedConfig; } // 创建画布容器 createCanvas() { const canvas = document.createElement('div'); canvas.style.cssText = ` position: relative; width: ${this.config.width}px; height: ${this.config.height}px; background: ${this.config.backgroundOptions.color}; overflow: hidden; font-family: Arial, sans-serif; `; // 应用渐变背景 if (this.config.backgroundOptions.gradient) { const gradient = this.createGradientStyle(this.config.backgroundOptions.gradient); canvas.style.background = gradient; } // 应用圆角 if (this.config.exportOptions.borderRadius > 0) { canvas.style.borderRadius = `${this.config.exportOptions.borderRadius}px`; } return canvas; } // 创建渐变样式 createGradientStyle(gradient) { const { type, rotation, colorStops } = gradient; const stops = colorStops.map((stop) => `${stop.color} ${stop.offset * 100}%`).join(', '); if (type === 'linear') { return `linear-gradient(${rotation}deg, ${stops})`; } else { return `radial-gradient(circle, ${stops})`; } } // 获取object-fit样式 getObjectFitStyle(mode) { switch (mode) { case 'fill': case 'stretch': return 'fill'; case 'contain': return 'contain'; case 'cover': return 'cover'; default: return 'fill'; } } // 添加背景图片 async addBackgrounds(canvas) { if (!this.config.backgrounds || this.config.backgrounds.length === 0) return; const loadPromises = this.config.backgrounds.map(async (bg, index) => { return new Promise((resolve) => { const img = document.createElement('img'); img.style.cssText = ` position: absolute; left: ${bg.position.x}px; top: ${bg.position.y}px; width: ${bg.size.width}px; height: ${bg.size.height}px; z-index: ${bg.zIndex}; opacity: ${bg.opacity}; object-fit: ${this.getObjectFitStyle(bg.mode)}; pointer-events: none; background-repeat: no-repeat; `; img.onload = () => { console.log(`背景图片 ${index + 1} 加载成功`); canvas.appendChild(img); resolve(); }; img.onerror = (error) => { console.warn(`背景图片 ${index + 1} 加载失败:`, error); resolve(); // 继续执行,不阻塞其他元素 }; // 设置crossOrigin在设置src之前 img.crossOrigin = 'anonymous'; img.src = bg.src; // 添加超时处理 setTimeout(() => { if (!img.complete) { console.warn(`背景图片 ${index + 1} 加载超时`); resolve(); } }, 10000); // 10秒超时 }); }); await Promise.all(loadPromises); } // 添加二维码 - 使用配置中的位置和尺寸 async addQRCode(canvas) { const QRCodeStylingClass = QRCodeStyling; return new Promise((resolve, reject) => { try { // 使用配置中的二维码位置和尺寸 const qrX = this.config.qrPosition.x; const qrY = this.config.qrPosition.y; const qrWidth = this.config.qrSize.width; const qrHeight = this.config.qrSize.height; console.log(`二维码配置: 位置(${qrX}, ${qrY}), 尺寸(${qrWidth}x${qrHeight})`); // 创建二维码容器 const qrContainer = document.createElement('div'); qrContainer.style.cssText = ` position: absolute; left: ${qrX}px; top: ${qrY}px; width: ${qrWidth}px; height: ${qrHeight}px; z-index: 100; `; // 配置二维码样式 const qrConfig = { width: qrWidth, height: qrHeight, type: 'canvas', data: this.config.text, image: this.config.logo?.src, qrOptions: this.config.qrOptions, imageOptions: { ...this.config.imageOptions, imageSize: this.config.logo?.size ? this.config.logo.size / 100 : this.config.imageOptions.imageSize, }, dotsOptions: this.config.dotsOptions, backgroundOptions: this.config.backgroundOptions, cornersSquareOptions: this.config.cornersSquareOptions, cornersDotOptions: this.config.cornersDotOptions, }; this.qrCode = new QRCodeStylingClass(qrConfig); // 渲染二维码 this.qrCode.append(qrContainer); canvas.appendChild(qrContainer); // 等待二维码渲染完成 setTimeout(() => { console.log('二维码渲染完成'); resolve(); }, 500); } catch (error) { console.error('二维码创建失败:', error); reject(error); } }); } // 添加文本 addTexts(canvas) { if (!this.config.texts || this.config.texts.length === 0) return; this.config.texts.forEach((text, index) => { const textElement = document.createElement('div'); textElement.textContent = text.content; textElement.style.cssText = ` position: absolute; left: ${text.position.x}px; top: ${text.position.y}px; font-size: ${text.fontSize}px; color: ${text.color}; font-family: ${text.fontFamily}; font-weight: ${text.fontWeight}; z-index: ${text.zIndex}; opacity: ${text.opacity}; text-align: ${text.textAlign || 'left'}; line-height: ${text.lineHeight || 1.2}; white-space: pre-wrap; word-break: break-word; pointer-events: none; `; console.log(`添加文本 ${index + 1}: "${text.content}" 位置(${text.position.x}, ${text.position.y})`); canvas.appendChild(textElement); }); } // 添加HTML模块 addHtmlModules(canvas) { if (!this.config.htmlModules || this.config.htmlModules.length === 0) return; this.config.htmlModules.forEach((module, index) => { const htmlElement = document.createElement('div'); htmlElement.innerHTML = module.content; htmlElement.style.cssText = ` position: absolute; left: ${module.position.x}px; top: ${module.position.y}px; width: ${module.size.width}px; height: ${module.size.height}px; z-index: ${module.zIndex}; opacity: ${module.opacity}; overflow: hidden; pointer-events: none; `; console.log(`添加HTML模块 ${index + 1} 位置(${module.position.x}, ${module.position.y})`); canvas.appendChild(htmlElement); }); } // 渲染完整画布 async render() { // 创建主画布 this.container = this.createCanvas(); // 添加到DOM(隐藏位置) this.container.style.position = 'absolute'; this.container.style.left = '-9999px'; this.container.style.top = '-9999px'; document.body.appendChild(this.container); try { console.log('开始渲染画布...', { canvas: `${this.config.width}x${this.config.height}`, qrPosition: this.config.qrPosition, qrSize: this.config.qrSize }); // 按层级顺序添加元素 console.log('添加背景图片...'); await this.addBackgrounds(this.container); console.log('添加二维码...'); await this.addQRCode(this.container); console.log('添加文本...'); this.addTexts(this.container); console.log('添加HTML模块...'); this.addHtmlModules(this.container); this.isRendered = true; console.log('画布渲染完成'); return this.container; } catch (error) { console.error('画布渲染失败:', error); this.cleanup(); throw error; } } // 导出为PNG async exportAsPNG(options) { if (!this.isRendered) { await this.render(); } if (!this.container) { throw new Error('画布未创建'); } const html2canvasOptions = { scale: options?.scale || 2, useCORS: options?.useCORS !== false, allowTaint: options?.allowTaint !== false, backgroundColor: null, logging: false, width: this.config.width, height: this.config.height, }; try { console.log('开始导出PNG...', html2canvasOptions); const canvas = await html2canvas(this.container, html2canvasOptions); console.log('html2canvas完成,画布尺寸:', canvas.width, 'x', canvas.height); return new Promise((resolve, reject) => { canvas.toBlob((blob) => { if (blob) { console.log('PNG导出成功,大小:', blob.size, 'bytes'); resolve(blob); } else { reject(new Error('无法创建PNG Blob')); } }, 'image/png', options?.quality || this.config.exportOptions.quality); }); } catch (error) { console.error('导出PNG失败:', error); throw new Error(`导出PNG失败: ${error}`); } } // 导出为DataURL async exportAsDataURL(options) { if (!this.isRendered) { await this.render(); } if (!this.container) { throw new Error('画布未创建'); } const html2canvasOptions = { scale: options?.scale || 2, useCORS: options?.useCORS !== false, allowTaint: options?.allowTaint !== false, backgroundColor: null, logging: false, width: this.config.width, height: this.config.height, }; try { const canvas = await html2canvas(this.container, html2canvasOptions); return canvas.toDataURL('image/png', options?.quality || this.config.exportOptions.quality); } catch (error) { throw new Error(`导出DataURL失败: ${error}`); } } // 下载PNG文件 async downloadAsPNG(filename, options) { const blob = await this.exportAsPNG(options); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename || `qr-code-${Date.now()}.png`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // 获取预览元素(用于显示) getPreviewElement() { return this.container; } // 更新配置 updateConfig(newConfig) { // 如果更新了画布尺寸但没有更新二维码位置/尺寸,则自动重新计算 if ((newConfig.width || newConfig.height) && (!newConfig.qrPosition && !newConfig.qrSize)) { const width = newConfig.width || this.config.width; const height = newConfig.height || this.config.height; const autoSettings = calculateDefaultQRSettings(width, height); newConfig.qrPosition = autoSettings.qrPosition; newConfig.qrSize = autoSettings.qrSize; } this.config = { ...this.config, ...newConfig }; this.isRendered = false; } // 获取当前配置 getConfig() { return { ...this.config }; } // 获取配置JSON字符串 getConfigString() { return JSON.stringify(this.config, null, 2); } // 从配置字符串加载 loadFromConfigString(configString) { try { const config = JSON.parse(configString); this.updateConfig(config); } catch (error) { throw new Error(`无法解析配置字符串: ${error}`); } } // 验证配置 validateConfig() { const errors = []; if (!this.config.text) { errors.push('二维码内容不能为空'); } if (this.config.width <= 0 || this.config.height <= 0) { errors.push('画布尺寸必须大于0'); } // 验证二维码位置和尺寸 if (!this.config.qrPosition) { errors.push('二维码位置未设置'); } else { if (this.config.qrPosition.x < 0 || this.config.qrPosition.y < 0) { errors.push('二维码位置不能为负数'); } if (this.config.qrPosition.x >= this.config.width || this.config.qrPosition.y >= this.config.height) { errors.push('二维码位置超出画布范围'); } } if (!this.config.qrSize) { errors.push('二维码尺寸未设置'); } else { if (this.config.qrSize.width <= 0 || this.config.qrSize.height <= 0) { errors.push('二维码尺寸必须大于0'); } if (this.config.qrPosition && (this.config.qrPosition.x + this.config.qrSize.width > this.config.width || this.config.qrPosition.y + this.config.qrSize.height > this.config.height)) { errors.push('二维码超出画布边界'); } } if (this.config.exportOptions.quality < 0 || this.config.exportOptions.quality > 1) { errors.push('导出质量必须在0-1之间'); } // 验证背景图片配置 if (this.config.backgrounds && this.config.backgrounds.length > 0) { this.config.backgrounds.forEach((bg, index) => { if (!bg.src) { errors.push(`背景图片 ${index + 1} 缺少src属性`); } if (bg.size.width <= 0 || bg.size.height <= 0) { errors.push(`背景图片 ${index + 1} 尺寸必须大于0`); } if (bg.opacity < 0 || bg.opacity > 1) { errors.push(`背景图片 ${index + 1} 透明度必须在0-1之间`); } }); } return { isValid: errors.length === 0, errors, }; } // 清理资源 cleanup() { if (this.container && this.container.parentNode) { this.container.parentNode.removeChild(this.container); } this.container = null; this.qrCode = null; this.isRendered = false; } // 销毁实例 destroy() { this.cleanup(); } } // 便捷函数:创建QR生成器 const createQRGenerator = (config) => { return new QRGenerator(config); }; // 便捷函数:快速生成并导出DataURL const generateQRAsDataURL = async (config, exportOptions) => { const generator = createQRGenerator(config); try { return await generator.exportAsDataURL(exportOptions); } finally { generator.destroy(); } }; // 便捷函数:快速生成并下载 const generateAndDownloadQR = async (config, filename, exportOptions) => { const generator = createQRGenerator(config); try { await generator.downloadAsPNG(filename, exportOptions); } finally { generator.destroy(); } }; // 类型导出 // export type { QRGeneratorConfig }; // 版本信息 const version = '1.0.0'; exports.QRGenerator = QRGenerator; exports.createQRGenerator = createQRGenerator; exports.defaultConfig = defaultConfig; exports.generateAndDownloadQR = generateAndDownloadQR; exports.generateQRAsDataURL = generateQRAsDataURL; exports.version = version;