UNPKG

id-scanner-lib

Version:

Browser-based ID card, QR code, and face recognition scanner with liveness detection

430 lines (387 loc) 10.1 kB
/** * @file 工具集 * @description 提供通用工具函数 * @module utils */ export * from './error-handler'; export * from './retry'; export * from './canvas-pool'; /** * 创建延迟Promise * @param ms 延迟毫秒数 */ export function delay(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } /** * 节流函数 * @param fn 要节流的函数 * @param wait 等待时间(ms) */ export function throttle<T extends (...args: any[]) => any>( fn: T, wait: number ): (...args: Parameters<T>) => ReturnType<T> | undefined { let lastTime = 0; let lastResult: ReturnType<T>; return function(this: any, ...args: Parameters<T>): ReturnType<T> | undefined { const now = Date.now(); if (now - lastTime >= wait) { lastTime = now; lastResult = fn.apply(this, args); } return lastResult; }; } /** * 防抖函数 * @param fn 要防抖的函数 * @param wait 等待时间(ms) * @param immediate 是否立即执行 */ export function debounce<T extends (...args: any[]) => any>( fn: T, wait: number, immediate: boolean = false ): (...args: Parameters<T>) => void { let timeout: number | null = null; return function(this: any, ...args: Parameters<T>): void { const callNow = immediate && !timeout; if (timeout !== null) { clearTimeout(timeout); } timeout = window.setTimeout(() => { timeout = null; if (!immediate) { fn.apply(this, args); } }, wait); if (callNow) { fn.apply(this, args); } }; } /** * 格式化字节大小 * @param bytes 字节数 * @param decimals 小数位数 */ export function formatBytes(bytes: number, decimals: number = 2): string { if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } /** * 生成UUID */ export function generateUUID(): string { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } /** * 检查浏览器支持的功能 */ export const browserCapabilities = { /** * 检查是否支持摄像头 */ hasCamera(): boolean { return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia); }, /** * 检查是否支持WebAssembly */ hasWasm(): boolean { return typeof WebAssembly === 'object' && typeof WebAssembly.compile === 'function' && typeof WebAssembly.instantiate === 'function'; }, /** * 检查是否支持WebWorker */ hasWebWorker(): boolean { return typeof Worker === 'function'; }, /** * 检查是否支持WebGL */ hasWebGL(): boolean { try { const canvas = document.createElement('canvas'); return !!( window.WebGLRenderingContext && (canvas.getContext('webgl') || canvas.getContext('experimental-webgl')) ); } catch (e) { return false; } }, /** * 检查是否支持SharedArrayBuffer */ hasSharedArrayBuffer(): boolean { return typeof SharedArrayBuffer === 'function'; }, /** * 检查是否支持特定的功能 * @param feature 功能名称 */ supports(feature: string): boolean { switch (feature.toLowerCase()) { case 'camera': return this.hasCamera(); case 'wasm': return this.hasWasm(); case 'webworker': case 'worker': return this.hasWebWorker(); case 'webgl': case 'gl': return this.hasWebGL(); case 'sharedarraybuffer': case 'sab': return this.hasSharedArrayBuffer(); default: return false; } } }; /** * 数组分块 * @param array 要分块的数组 * @param chunkSize 每块大小 */ export function chunk<T>(array: T[], chunkSize: number): T[][] { if (chunkSize < 1) throw new Error('Chunk size must be greater than 0'); const result: T[][] = []; for (let i = 0; i < array.length; i += chunkSize) { result.push(array.slice(i, i + chunkSize)); } return result; } /** * 安全解析JSON * @param text JSON字符串 * @param fallback 解析失败时的默认值 */ export function safeParseJSON<T = any>(text: string, fallback: T): T { try { return JSON.parse(text) as T; } catch (e) { return fallback; } } /** * 限制值在指定范围内 * @param value 要限制的值 * @param min 最小值 * @param max 最大值 */ export function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } /** * 等待加载图片 * @param url 图片URL */ export function loadImage(url: string): Promise<HTMLImageElement> { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = () => reject(new Error(`Failed to load image: ${url}`)); img.src = url; }); } /** * 将Blob转换为Base64 * @param blob Blob对象 */ export function blobToBase64(blob: Blob): Promise<string> { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result as string); reader.onerror = () => reject(reader.error); reader.readAsDataURL(blob); }); } /** * 将Base64转换为Blob * @param base64 Base64字符串 * @param contentType 内容类型 */ export function base64ToBlob(base64: string, contentType: string = ''): Blob { const byteString = atob(base64.split(',')[1]); const ab = new ArrayBuffer(byteString.length); const ia = new Uint8Array(ab); for (let i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i); } return new Blob([ab], { type: contentType }); } /** * 获取媒体约束 * @param width 宽度 * @param height 高度 * @param facingMode 前后置摄像头 * @param frameRate 帧率 */ export function getMediaConstraints( width: number = 1280, height: number = 720, facingMode: 'user' | 'environment' = 'environment', frameRate: number = 30 ): MediaStreamConstraints { return { video: { width: { ideal: width }, height: { ideal: height }, facingMode: { ideal: facingMode }, frameRate: { ideal: frameRate } }, audio: false }; } /** * 检查URL是否有效 * @param url 要检查的URL */ export function isValidUrl(url: string): boolean { try { new URL(url); return true; } catch (e) { return false; } } /** * DOM帮助函数 */ export const dom = { /** * 创建元素 * @param tag 标签名 * @param attributes 属性 * @param children 子元素 */ createElement<K extends keyof HTMLElementTagNameMap>( tag: K, attributes: Record<string, any> = {}, children: (string | Node)[] = [] ): HTMLElementTagNameMap[K] { const element = document.createElement(tag); // 设置属性 Object.entries(attributes).forEach(([key, value]) => { if (key === 'style' && typeof value === 'object') { Object.assign(element.style, value); } else if (key.startsWith('on') && typeof value === 'function') { element.addEventListener(key.substring(2).toLowerCase(), value); } else if (key === 'className') { element.className = value; } else { element.setAttribute(key, value); } }); // 添加子元素 children.forEach(child => { if (typeof child === 'string') { element.appendChild(document.createTextNode(child)); } else { element.appendChild(child); } }); return element; }, /** * 查找元素 * @param selector 选择器 * @param parent 父元素 */ find<E extends Element = Element>( selector: string, parent: Document | Element = document ): E | null { return parent.querySelector<E>(selector); }, /** * 查找所有元素 * @param selector 选择器 * @param parent 父元素 */ findAll<E extends Element = Element>( selector: string, parent: Document | Element = document ): E[] { return Array.from(parent.querySelectorAll<E>(selector)); }, /** * 添加事件监听器 * @param element 元素 * @param event 事件名称 * @param handler 处理函数 * @param options 选项 */ on<K extends keyof HTMLElementEventMap>( element: HTMLElement, event: K, handler: (event: HTMLElementEventMap[K]) => any, options?: AddEventListenerOptions ): void { element.addEventListener(event, handler as EventListener, options); }, /** * 移除事件监听器 * @param element 元素 * @param event 事件名称 * @param handler 处理函数 * @param options 选项 */ off<K extends keyof HTMLElementEventMap>( element: HTMLElement, event: K, handler: (event: HTMLElementEventMap[K]) => any, options?: EventListenerOptions ): void { element.removeEventListener(event, handler as EventListener, options); }, /** * 设置样式 * @param element 元素 * @param styles 样式对象 */ setStyles( element: HTMLElement, styles: Partial<CSSStyleDeclaration> ): void { Object.assign(element.style, styles); }, /** * 添加类名 * @param element 元素 * @param classNames 类名 */ addClass( element: HTMLElement, ...classNames: string[] ): void { element.classList.add(...classNames); }, /** * 移除类名 * @param element 元素 * @param classNames 类名 */ removeClass( element: HTMLElement, ...classNames: string[] ): void { element.classList.remove(...classNames); }, /** * 判断是否包含类名 * @param element 元素 * @param className 类名 */ hasClass( element: HTMLElement, className: string ): boolean { return element.classList.contains(className); } };