UNPKG

id-scanner-lib

Version:

一款纯前端实现的TypeScript身份证&二维码识别库,无需后端支持,所有处理在浏览器端完成,新增图像批处理与优化

990 lines (883 loc) 29.8 kB
/** * @file ID扫描识别库主入口文件 * @description 提供身份证识别与二维码、条形码扫描功能的纯前端TypeScript库 * @module IDScannerLib * @version 1.3.0 * @license MIT */ import { Camera, CameraOptions } from "./utils/camera" import { IDCardInfo, DetectionResult } from "./utils/types" import { ImageProcessor, ImageProcessorOptions, ImageCompressionOptions, } from "./utils/image-processing" // 先只导入类型定义,不导入实际实现 import type { QRScannerOptions } from "./scanner/qr-scanner" import type { BarcodeScannerOptions } from "./scanner/barcode-scanner" // 导入防伪检测器 import { AntiFakeDetector, AntiFakeDetectionResult, } from "./id-recognition/anti-fake-detector" /** * IDScanner配置选项接口 */ export interface IDScannerOptions { cameraOptions?: CameraOptions qrScannerOptions?: QRScannerOptions barcodeScannerOptions?: BarcodeScannerOptions onQRCodeScanned?: (result: string) => void onBarcodeScanned?: (result: string) => void onIDCardScanned?: (info: IDCardInfo) => void onImageProcessed?: (processedImage: ImageData | File) => void onAntiFakeDetected?: (result: AntiFakeDetectionResult) => void onError?: (error: Error) => void } /** * IDScanner 主类 * * 整合二维码、条形码扫描和身份证识别功能,提供统一的接口 * 使用动态导入实现按需加载 */ export class IDScanner { private camera: Camera private scanMode: "qr" | "barcode" | "idcard" = "qr" private videoElement: HTMLVideoElement | null = null private scanning = false private qrModule: any = null private ocrModule: any = null private scanTimer: number | null = null private isQRModuleLoaded: boolean = false private isOCRModuleLoaded: boolean = false // 新增防伪检测器 private antiFakeDetector: AntiFakeDetector | null = null private isAntiFakeModuleLoaded: boolean = false /** * 构造函数 * @param options 配置选项 */ constructor(private options: IDScannerOptions = {}) { this.camera = new Camera(options.cameraOptions) } /** * 初始化模块 * 根据需要初始化OCR引擎和防伪检测模块 */ async initialize(): Promise<void> { try { // 预加载OCR模块但不初始化 if (!this.isOCRModuleLoaded) { // 动态导入OCR模块 const OCRModule = await import("./ocr-module").then((m) => m.OCRModule) this.ocrModule = new OCRModule({ cameraOptions: this.options.cameraOptions, onIDCardScanned: this.options.onIDCardScanned, onError: this.options.onError, }) this.isOCRModuleLoaded = true // 初始化OCR模块 await this.ocrModule.initialize() } // 初始化防伪检测模块 if (!this.isAntiFakeModuleLoaded) { this.antiFakeDetector = new AntiFakeDetector() this.isAntiFakeModuleLoaded = true } console.log("IDScanner初始化完成") } catch (error) { console.error("初始化失败:", error) this.handleError(error as Error) throw error } } /** * 初始化OCR模块 */ private async initOCRModule(): Promise<void> { if (this.isOCRModuleLoaded) return try { // 动态导入OCR模块 const OCRModule = await import("./ocr-module").then((m) => m.OCRModule) this.ocrModule = new OCRModule({ cameraOptions: this.options.cameraOptions, onIDCardScanned: this.options.onIDCardScanned, onError: this.options.onError, }) this.isOCRModuleLoaded = true // 初始化OCR模块 await this.ocrModule.initialize() } catch (error) { console.error("OCR模块初始化失败:", error) throw error } } /** * 启动二维码扫描 * @param videoElement HTML视频元素 */ async startQRScanner(videoElement: HTMLVideoElement): Promise<void> { this.stop() this.videoElement = videoElement this.scanMode = "qr" try { // 动态加载二维码模块 if (!this.isQRModuleLoaded) { const ScannerModule = await import("./qr-module").then( (m) => m.ScannerModule ) this.qrModule = new ScannerModule({ cameraOptions: this.options.cameraOptions, qrScannerOptions: this.options.qrScannerOptions, barcodeScannerOptions: this.options.barcodeScannerOptions, onQRCodeScanned: this.options.onQRCodeScanned, onBarcodeScanned: this.options.onBarcodeScanned, onError: this.options.onError, }) this.isQRModuleLoaded = true } await this.qrModule.startQRScanner(videoElement) } catch (error) { this.handleError(error as Error) } } /** * 启动条形码扫描 * @param videoElement HTML视频元素 */ async startBarcodeScanner(videoElement: HTMLVideoElement): Promise<void> { this.stop() this.videoElement = videoElement this.scanMode = "barcode" try { // 动态加载二维码模块 if (!this.isQRModuleLoaded) { const ScannerModule = await import("./qr-module").then( (m) => m.ScannerModule ) this.qrModule = new ScannerModule({ cameraOptions: this.options.cameraOptions, qrScannerOptions: this.options.qrScannerOptions, barcodeScannerOptions: this.options.barcodeScannerOptions, onQRCodeScanned: this.options.onQRCodeScanned, onBarcodeScanned: this.options.onBarcodeScanned, onError: this.options.onError, }) this.isQRModuleLoaded = true } await this.qrModule.startBarcodeScanner(videoElement) } catch (error) { this.handleError(error as Error) } } /** * 启动身份证扫描 * @param videoElement HTML视频元素 */ async startIDCardScanner(videoElement: HTMLVideoElement): Promise<void> { this.stop() this.videoElement = videoElement this.scanMode = "idcard" try { // 检查OCR模块是否已加载,若未加载则自动初始化 if (!this.isOCRModuleLoaded) { await this.initialize() } await this.ocrModule.startIDCardScanner(videoElement) } catch (error) { this.handleError(error as Error) } } /** * 停止扫描 */ stop(): void { if (this.scanMode === "qr" || this.scanMode === "barcode") { if (this.qrModule) { this.qrModule.stop() } } else if (this.scanMode === "idcard") { if (this.ocrModule) { this.ocrModule.stop() } } } /** * 处理错误 */ private handleError(error: Error): void { if (this.options.onError) { this.options.onError(error) } else { console.error("IDScanner错误:", error) } } /** * 释放资源 */ async terminate(): Promise<void> { this.stop() // 释放OCR资源 if (this.isOCRModuleLoaded && this.ocrModule) { await this.ocrModule.terminate() this.ocrModule = null this.isOCRModuleLoaded = false } // 释放QR扫描资源 if (this.isQRModuleLoaded && this.qrModule) { this.qrModule = null this.isQRModuleLoaded = false } // 释放防伪检测资源 if (this.antiFakeDetector) { this.antiFakeDetector.dispose() this.antiFakeDetector = null this.isAntiFakeModuleLoaded = false } } /** * 处理图片中的二维码 * @param imageSource 图片源,可以是Image元素、Canvas元素或URL字符串 * @returns 返回Promise,解析为扫描结果 */ async processQRCodeImage( imageSource: HTMLImageElement | HTMLCanvasElement | string | File ): Promise<string> { try { // 动态加载二维码模块 if (!this.isQRModuleLoaded) { const ScannerModule = await import("./qr-module").then( (m) => m.ScannerModule ) this.qrModule = new ScannerModule({ qrScannerOptions: this.options.qrScannerOptions, onQRCodeScanned: this.options.onQRCodeScanned, onError: this.options.onError, }) this.isQRModuleLoaded = true } // 处理不同类型的图片源 let imageElement: HTMLImageElement if (imageSource instanceof File) { // 如果是File对象,创建新的Image元素并加载图片 imageElement = new Image() imageElement.crossOrigin = "anonymous" // 处理跨域图片 const url = URL.createObjectURL(imageSource) await new Promise((resolve, reject) => { imageElement.onload = resolve imageElement.onerror = reject imageElement.src = url }) // 使用后释放URL对象 URL.revokeObjectURL(url) } else if (typeof imageSource === "string") { // 如果是URL字符串,创建新的Image元素并加载图片 imageElement = new Image() imageElement.crossOrigin = "anonymous" // 处理跨域图片 await new Promise((resolve, reject) => { imageElement.onload = resolve imageElement.onerror = reject imageElement.src = imageSource }) } else if (imageSource instanceof HTMLImageElement) { // 如果已经是Image元素,直接使用 imageElement = imageSource } else if (imageSource instanceof HTMLCanvasElement) { // 如果是Canvas元素,创建Image并从Canvas获取数据 imageElement = new Image() imageElement.src = imageSource.toDataURL() await new Promise((resolve) => { imageElement.onload = resolve }) } else { throw new Error("不支持的图片源类型") } // 获取图像数据 const canvas = document.createElement("canvas") canvas.width = imageElement.naturalWidth canvas.height = imageElement.naturalHeight const ctx = canvas.getContext("2d") if (!ctx) { throw new Error("无法创建Canvas上下文") } ctx.drawImage(imageElement, 0, 0) const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) // 使用QR模块处理图像 return this.qrModule.processQRCodeImage(imageData) } catch (error) { this.handleError(error as Error) throw error } } /** * 处理图片中的条形码 * @param imageSource 图片源,可以是Image元素、Canvas元素或URL字符串 * @returns 返回Promise,解析为扫描结果 */ async processBarcodeImage( imageSource: HTMLImageElement | HTMLCanvasElement | string | File ): Promise<string> { try { // 动态加载二维码模块 if (!this.isQRModuleLoaded) { const ScannerModule = await import("./qr-module").then( (m) => m.ScannerModule ) this.qrModule = new ScannerModule({ barcodeScannerOptions: this.options.barcodeScannerOptions, onBarcodeScanned: this.options.onBarcodeScanned, onError: this.options.onError, }) this.isQRModuleLoaded = true } // 处理不同类型的图片源 let imageElement: HTMLImageElement if (imageSource instanceof File) { // 如果是File对象,创建新的Image元素并加载图片 imageElement = new Image() imageElement.crossOrigin = "anonymous" // 处理跨域图片 const url = URL.createObjectURL(imageSource) await new Promise((resolve, reject) => { imageElement.onload = resolve imageElement.onerror = reject imageElement.src = url }) // 使用后释放URL对象 URL.revokeObjectURL(url) } else if (typeof imageSource === "string") { // 如果是URL字符串,创建新的Image元素并加载图片 imageElement = new Image() imageElement.crossOrigin = "anonymous" // 处理跨域图片 await new Promise((resolve, reject) => { imageElement.onload = resolve imageElement.onerror = reject imageElement.src = imageSource }) } else if (imageSource instanceof HTMLImageElement) { // 如果已经是Image元素,直接使用 imageElement = imageSource } else if (imageSource instanceof HTMLCanvasElement) { // 如果是Canvas元素,创建Image并从Canvas获取数据 imageElement = new Image() imageElement.src = imageSource.toDataURL() await new Promise((resolve) => { imageElement.onload = resolve }) } else { throw new Error("不支持的图片源类型") } // 获取图像数据 const canvas = document.createElement("canvas") canvas.width = imageElement.naturalWidth canvas.height = imageElement.naturalHeight const ctx = canvas.getContext("2d") if (!ctx) { throw new Error("无法创建Canvas上下文") } ctx.drawImage(imageElement, 0, 0) const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) // 使用Barcode模块处理图像 return this.qrModule.processBarcodeImage(imageData) } catch (error) { this.handleError(error as Error) throw error } } /** * 处理图片中的身份证 * @param imageSource 图片源,可以是Image元素、Canvas元素或URL字符串 * @returns 返回Promise,解析为身份证信息 */ async processIDCardImage( imageSource: HTMLImageElement | HTMLCanvasElement | string | File ): Promise<IDCardInfo> { if (!this.isOCRModuleLoaded) { await this.initOCRModule() } try { // 处理不同类型的图片源 let imageElement: HTMLImageElement if (imageSource instanceof File) { // 如果是File对象,先进行压缩 const compressedFile = await ImageProcessor.compressImage(imageSource, { maxSizeMB: 2, // 最大2MB maxWidthOrHeight: 1800, // 最大尺寸 useWebWorker: true, }) // 创建新的Image元素并加载图片 imageElement = new Image() imageElement.crossOrigin = "anonymous" // 处理跨域图片 const url = URL.createObjectURL(compressedFile) await new Promise((resolve, reject) => { imageElement.onload = resolve imageElement.onerror = reject imageElement.src = url }) // 使用后释放URL对象 URL.revokeObjectURL(url) } else if (typeof imageSource === "string") { // 如果是URL字符串,创建新的Image元素并加载图片 imageElement = new Image() imageElement.crossOrigin = "anonymous" // 处理跨域图片 await new Promise((resolve, reject) => { imageElement.onload = resolve imageElement.onerror = reject imageElement.src = imageSource }) } else if (imageSource instanceof HTMLImageElement) { // 如果已经是Image元素,直接使用 imageElement = imageSource } else if (imageSource instanceof HTMLCanvasElement) { // 如果是Canvas元素,创建Image并从Canvas获取数据 imageElement = new Image() imageElement.src = imageSource.toDataURL() await new Promise((resolve) => { imageElement.onload = resolve }) } else { throw new Error("不支持的图片源类型") } // 获取图像数据 const canvas = document.createElement("canvas") canvas.width = imageElement.naturalWidth canvas.height = imageElement.naturalHeight const ctx = canvas.getContext("2d") if (!ctx) { throw new Error("无法创建Canvas上下文") } ctx.drawImage(imageElement, 0, 0) const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) // 对图像进行预处理,提高识别率 const enhancedImageData = ImageProcessor.batchProcess(imageData, { brightness: 10, contrast: 15, sharpen: true, }) // 使用OCR模块处理图像 const idInfo = await this.ocrModule.processIDCard(enhancedImageData) // 进行防伪检测并将结果添加到身份证信息中 if (this.isAntiFakeModuleLoaded && this.antiFakeDetector) { try { const result = await this.antiFakeDetector.detect(enhancedImageData) // 将防伪检测结果添加到身份证信息对象中 const extendedInfo = idInfo as any extendedInfo.antiFakeResult = result // 触发防伪检测回调 if (this.options.onAntiFakeDetected) { this.options.onAntiFakeDetected(result) } } catch (error) { console.warn("身份证防伪检测失败:", error) } } return idInfo } catch (error) { this.handleError(error as Error) throw error } } /** * 批量处理图像 * @param imageSource 图片源,可以是Image元素、Canvas元素、URL字符串或File对象 * @param options 图像处理选项 * @param outputFormat 输出格式,'imagedata'或'file' * @returns 返回Promise,解析为处理后的ImageData或File */ async processImage( imageSource: HTMLImageElement | HTMLCanvasElement | string | File, options: ImageProcessorOptions = {}, outputFormat: "imagedata" | "file" = "imagedata" ): Promise<ImageData | File> { try { // 处理不同类型的图片源 let imageData: ImageData if (imageSource instanceof File) { // 如果是File对象,先进行压缩 const compressedFile = await ImageProcessor.compressImage(imageSource, { maxSizeMB: 2, // 最大2MB maxWidthOrHeight: 1920, // 最大尺寸 useWebWorker: true, }) // 从File创建ImageData imageData = await ImageProcessor.createImageDataFromFile(compressedFile) } else if (typeof imageSource === "string") { // 如果是URL字符串,创建新的Image元素并加载图片 const imageElement = new Image() imageElement.crossOrigin = "anonymous" // 处理跨域图片 await new Promise((resolve, reject) => { imageElement.onload = resolve imageElement.onerror = reject imageElement.src = imageSource }) // 获取图像数据 const canvas = document.createElement("canvas") canvas.width = imageElement.naturalWidth canvas.height = imageElement.naturalHeight const ctx = canvas.getContext("2d") if (!ctx) { throw new Error("无法创建Canvas上下文") } ctx.drawImage(imageElement, 0, 0) imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) } else if (imageSource instanceof HTMLImageElement) { // 如果是Image元素,从它创建ImageData const canvas = document.createElement("canvas") canvas.width = imageSource.naturalWidth canvas.height = imageSource.naturalHeight const ctx = canvas.getContext("2d") if (!ctx) { throw new Error("无法创建Canvas上下文") } ctx.drawImage(imageSource, 0, 0) imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) } else if (imageSource instanceof HTMLCanvasElement) { // 如果是Canvas元素,直接获取其ImageData const ctx = imageSource.getContext("2d") if (!ctx) { throw new Error("无法获取Canvas上下文") } imageData = ctx.getImageData( 0, 0, imageSource.width, imageSource.height ) } else { throw new Error("不支持的图片源类型") } // 进行图像处理 const processedImageData = ImageProcessor.batchProcess(imageData, options) // 根据需要的输出格式返回结果 if (outputFormat === "file") { // 将ImageData转换为File const file = await ImageProcessor.imageDataToFile( processedImageData, "processed_image.jpg", "image/jpeg", 0.85 ) // 触发回调 if (this.options.onImageProcessed) { this.options.onImageProcessed(file) } return file } else { // 触发回调 if (this.options.onImageProcessed) { this.options.onImageProcessed(processedImageData) } return processedImageData } } catch (error) { this.handleError(error as Error) throw error } } /** * 压缩图片 * @param file 要压缩的图片文件 * @param options 压缩选项 * @returns 返回Promise,解析为压缩后的文件 */ async compressImage( file: File, options?: ImageCompressionOptions ): Promise<File> { try { return await ImageProcessor.compressImage(file, options) } catch (error) { this.handleError(error as Error) throw error } } /** * 身份证防伪检测 * @param imageSource 图片源 * @returns 防伪检测结果 */ async detectIDCardAntiFake( imageSource: HTMLImageElement | HTMLCanvasElement | string | File ): Promise<AntiFakeDetectionResult> { if (!this.isAntiFakeModuleLoaded || !this.antiFakeDetector) { await this.initialize() if (!this.antiFakeDetector) { throw new Error("防伪检测模块初始化失败") } } try { // 转换输入为ImageData let imageData: ImageData if (typeof imageSource === "string") { // 处理URL或Base64 const img = new Image() await new Promise<void>((resolve, reject) => { img.onload = () => resolve() img.onerror = () => reject(new Error("图像加载失败")) img.src = imageSource }) const canvas = document.createElement("canvas") canvas.width = img.width canvas.height = img.height const ctx = canvas.getContext("2d") if (!ctx) { throw new Error("无法创建Canvas上下文") } ctx.drawImage(img, 0, 0) imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) } else if (imageSource instanceof File) { // 处理文件 imageData = await ImageProcessor.createImageDataFromFile(imageSource) } else if (imageSource instanceof HTMLImageElement) { // 处理Image元素 const canvas = document.createElement("canvas") canvas.width = imageSource.width canvas.height = imageSource.height const ctx = canvas.getContext("2d") if (!ctx) { throw new Error("无法创建Canvas上下文") } ctx.drawImage(imageSource, 0, 0) imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) } else { // 处理Canvas元素 const ctx = (imageSource as HTMLCanvasElement).getContext("2d") if (!ctx) { throw new Error("无法获取Canvas上下文") } imageData = ctx.getImageData( 0, 0, imageSource.width, imageSource.height ) } // 执行防伪检测 const result = await this.antiFakeDetector.detect(imageData) // 触发回调 if (this.options.onAntiFakeDetected) { this.options.onAntiFakeDetected(result) } return result } catch (error) { this.handleError(error as Error) throw error } } } // 导出工具类和类型 export { Camera, CameraOptions } from "./utils/camera" export { ImageProcessor, ImageProcessorOptions, ImageCompressionOptions, } from "./utils/image-processing" export { IDCardInfo, DetectionResult } from "./utils/types" // 导出防伪检测相关类型和类 export { AntiFakeDetector, AntiFakeDetectionResult } // 为了向后兼容,我们创建一个演示类 export class IDScannerDemo { private scanner: IDScanner private currentMode: "qr" | "idcard" = "qr" private videoElement: HTMLVideoElement private resultElement: HTMLElement /** * 创建演示类实例 * @param videoElementId 视频元素ID * @param resultElementId 结果显示元素ID * @param switchButtonId 切换按钮ID * @param imageInputId 图片输入元素ID */ constructor( videoElementId: string, resultElementId: string, switchButtonId?: string, imageInputId?: string ) { this.videoElement = document.getElementById( videoElementId ) as HTMLVideoElement this.resultElement = document.getElementById(resultElementId) as HTMLElement // 创建扫描器实例 this.scanner = new IDScanner({ onQRCodeScanned: (result) => this.handleScanResult(result), onIDCardScanned: (info) => this.handleIDCardResult(info), onError: (error) => this.handleError(error), }) // 设置切换按钮事件 if (switchButtonId) { const switchButton = document.getElementById(switchButtonId) if (switchButton) { switchButton.addEventListener("click", () => this.toggleMode()) } } // 设置图片输入事件 if (imageInputId) { const imageInput = document.getElementById( imageInputId ) as HTMLInputElement if (imageInput) { imageInput.addEventListener("change", (e) => this.handleImageInput(e)) } } } /** * 初始化扫描器 */ async initialize(): Promise<void> { try { // 初始化身份证识别引擎 await this.scanner.initialize() // 默认启动二维码扫描 await this.startQRMode() } catch (error) { this.handleError(error as Error) } } /** * 切换扫描模式 */ async toggleMode(): Promise<void> { try { this.scanner.stop() if (this.currentMode === "qr") { this.currentMode = "idcard" await this.startIDCardMode() } else { this.currentMode = "qr" await this.startQRMode() } } catch (error) { this.handleError(error as Error) } } /** * 启动二维码扫描模式 */ private async startQRMode(): Promise<void> { try { this.updateResultDisplay("等待扫描二维码...") await this.scanner.startQRScanner(this.videoElement) } catch (error) { this.handleError(error as Error) } } /** * 启动身份证扫描模式 */ private async startIDCardMode(): Promise<void> { try { this.updateResultDisplay("等待扫描身份证...") await this.scanner.startIDCardScanner(this.videoElement) } catch (error) { this.handleError(error as Error) } } /** * 处理图片输入 */ private async handleImageInput(event: Event): Promise<void> { try { const input = event.target as HTMLInputElement if (!input.files || input.files.length === 0) { return } const file = input.files[0] this.updateResultDisplay("正在处理图片...") // 根据当前模式处理图片 if (this.currentMode === "qr") { const result = await this.scanner.processQRCodeImage(file) this.handleScanResult(result) } else { const info = await this.scanner.processIDCardImage(file) this.handleIDCardResult(info) } } catch (error) { this.handleError(error as Error) } } /** * 处理扫描结果 */ private handleScanResult(result: string): void { this.updateResultDisplay(` <h3>扫描结果:</h3> <p>${result}</p> `) } /** * 处理身份证识别结果 */ private handleIDCardResult(info: IDCardInfo): void { // 格式化显示身份证信息 const infoHtml = Object.entries(info) .filter(([key, value]) => value && key !== "antiFakeResult") // 过滤掉空值和防伪结果 .map(([key, value]) => { // 转换键名为中文显示 const keyMap: { [key: string]: string } = { name: "姓名", gender: "性别", nationality: "民族", birthDate: "出生日期", address: "地址", idNumber: "身份证号", issuingAuthority: "签发机关", validPeriod: "有效期限", } const displayKey = keyMap[key] || key return `<div><strong>${displayKey}:</strong> ${value}</div>` }) .join("") // 检查是否有防伪检测结果 let antiFakeHtml = "" const anyInfo = info as any if (anyInfo.antiFakeResult) { const antiFakeResult = anyInfo.antiFakeResult antiFakeHtml = ` <h3>防伪检测结果:</h3> <div style="color: ${antiFakeResult.isAuthentic ? "green" : "red"}"> ${ antiFakeResult.isAuthentic ? "✓ 身份证真实" : "⚠ 警告:可能为伪造证件" } </div> <div>置信度: ${(antiFakeResult.confidence * 100).toFixed(1)}%</div> <div>检测到的特征: ${ antiFakeResult.detectedFeatures.join(", ") || "无" }</div> <div>${antiFakeResult.message}</div> ` } this.updateResultDisplay(` <h3>身份证信息:</h3> ${infoHtml} ${antiFakeHtml} `) } /** * 处理错误 */ private handleError(error: Error): void { console.error("识别错误:", error) this.updateResultDisplay(` <div class="error"> <h3>错误:</h3> <p>${error.message}</p> </div> `) } /** * 更新结果显示 */ private updateResultDisplay(html: string): void { if (this.resultElement) { this.resultElement.innerHTML = html } } /** * 停止扫描 */ stop(): void { this.scanner.stop() } }