jz-h5-scancode
Version:
H5环境下的二维码扫描插件,提供与 uni.scanCode 完全兼容的 API 接口,支持摄像头实时扫码和图片扫码。v1.1.0优化性能,修复卡死问题
599 lines (522 loc) • 19.1 kB
JavaScript
/**
* UI管理器模块
* 负责创建和管理扫码界面UI
*/
class UIManager {
constructor() {
this.scannerContainer = null;
this.canvas = null;
this.video = null;
this.overlay = null;
this.scanLine = null;
this.scanLineAnimation = null;
this.scanFrameColor = '#00ff00'; // 默认绿色
}
/**
* 创建扫码器UI - 微信风格全屏界面
* @param {Object} options - UI配置选项
* @returns {HTMLCanvasElement} canvas元素
*/
createScannerUI(options = {}) {
// 设置扫描框颜色
if (options.scanFrameColor) {
this.scanFrameColor = options.scanFrameColor;
}
// 如果已经存在,先移除
this.removeScannerUI();
// 创建全屏容器
this.overlay = document.createElement('div');
this.overlay.className = 'jz-scanner-fullscreen';
this.overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #000;
z-index: 9999;
display: flex;
flex-direction: column;
`;
// 创建顶部导航栏
const navbar = document.createElement('div');
navbar.className = 'jz-scanner-navbar';
navbar.style.cssText = `
position: relative;
height: 60px;
background: linear-gradient(180deg, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.4) 100%);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
z-index: 10001;
`;
// 左上角返回按钮
const backButton = document.createElement('div');
backButton.className = 'jz-scanner-back';
backButton.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
<path d="M19 12H5M12 19l-7-7 7-7"/>
</svg>
`;
backButton.style.cssText = `
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 20px;
background: rgba(255,255,255,0.1);
transition: background 0.3s;
`;
// 标题
const title = document.createElement('div');
title.className = 'jz-scanner-title';
title.textContent = '扫一扫';
title.style.cssText = `
color: white;
font-size: 18px;
font-weight: bold;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
`;
// 右上角选择图片
const selectImage = document.createElement('div');
selectImage.className = 'jz-scanner-select';
selectImage.textContent = '相册';
selectImage.style.cssText = `
color: white;
font-size: 16px;
cursor: pointer;
padding: 8px 12px;
border-radius: 4px;
background: rgba(255,255,255,0.1);
transition: background 0.3s;
`;
// 组装导航栏
navbar.appendChild(backButton);
navbar.appendChild(title);
navbar.appendChild(selectImage);
// 创建扫码区域容器
const scanArea = document.createElement('div');
scanArea.className = 'jz-scanner-area';
scanArea.style.cssText = `
flex: 1;
position: relative;
overflow: hidden;
height: calc(100vh - 60px);
`;
// 创建video元素(用于显示摄像头画面)
this.video = document.createElement('video');
this.video.className = 'jz-scanner-video';
this.video.setAttribute('playsinline', true);
this.video.setAttribute('muted', true);
this.video.style.cssText = `
width: 100%;
height: 100%;
object-fit: cover;
background-color: #000;
`;
// 创建canvas(用于扫码识别)
this.canvas = document.createElement('canvas');
this.canvas.className = 'jz-scanner-canvas';
this.canvas.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
pointer-events: none;
`;
// 创建扫描框遮罩
const scanMask = document.createElement('div');
scanMask.className = 'jz-scanner-mask';
scanMask.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10000;
`;
// 创建扫描框
const scanBox = document.createElement('div');
scanBox.className = 'jz-scanner-box';
const boxSize = Math.min(window.innerWidth, window.innerHeight) * 0.7;
const boxTop = (window.innerHeight - 60 - boxSize) / 2;
const boxLeft = (window.innerWidth - boxSize) / 2;
scanBox.style.cssText = `
position: absolute;
top: ${boxTop}px;
left: ${boxLeft}px;
width: ${boxSize}px;
height: ${boxSize}px;
border: 2px solid rgba(255,255,255,0.5);
border-radius: 12px;
background: transparent;
box-shadow:
0 0 0 2000px rgba(0,0,0,0.5),
inset 0 0 0 1px rgba(255,255,255,0.2);
`;
// 创建四角标记
this.createCornerMarkers(scanBox, boxSize);
// 创建扫描线
this.scanLine = document.createElement('div');
this.scanLine.className = 'jz-scanner-line';
this.scanLine.style.cssText = `
position: absolute;
top: 10px;
left: 10px;
right: 10px;
height: 3px;
background: linear-gradient(90deg, transparent, ${this.scanFrameColor}, transparent);
border-radius: 2px;
box-shadow: 0 0 10px ${this.scanFrameColor};
`;
// 创建提示文字
const hint = document.createElement('div');
hint.className = 'jz-scanner-hint';
hint.textContent = '将二维码放入框内,即可自动扫描';
hint.style.cssText = `
position: absolute;
bottom: -60px;
left: 50%;
transform: translateX(-50%);
color: white;
font-size: 16px;
text-align: center;
white-space: nowrap;
`;
// 组装扫描框
scanBox.appendChild(this.scanLine);
scanBox.appendChild(hint);
scanMask.appendChild(scanBox);
// 组装扫码区域
scanArea.appendChild(this.video);
scanArea.appendChild(this.canvas);
scanArea.appendChild(scanMask);
// 组装完整界面(移除底部功能区)
this.overlay.appendChild(navbar);
this.overlay.appendChild(scanArea);
// 添加到body
document.body.appendChild(this.overlay);
// 添加事件监听(移除手电筒按钮参数)
this.addEventListeners(backButton, selectImage);
// 启动扫描线动画
this.startScanLineAnimation();
// 添加动画效果
this.addShowAnimation();
// 阻止页面滚动
document.body.style.overflow = 'hidden';
// 添加CSS样式
this.addGlobalStyles();
console.log('微信风格扫码器UI已创建');
return this.canvas;
}
/**
* 创建四角标记
*/
createCornerMarkers(scanBox, boxSize) {
const cornerSize = 20;
const cornerThickness = 4;
const corners = [
{ class: 'top-left', style: `top: -2px; left: -2px;` },
{ class: 'top-right', style: `top: -2px; right: -2px;` },
{ class: 'bottom-left', style: `bottom: -2px; left: -2px;` },
{ class: 'bottom-right', style: `bottom: -2px; right: -2px;` }
];
corners.forEach(corner => {
const marker = document.createElement('div');
marker.className = `jz-corner ${corner.class}`;
marker.style.cssText = `
position: absolute;
${corner.style}
width: ${cornerSize}px;
height: ${cornerSize}px;
border: ${cornerThickness}px solid ${this.scanFrameColor};
border-radius: 4px;
`;
// 根据位置设置边框
if (corner.class === 'top-left') {
marker.style.borderRight = 'none';
marker.style.borderBottom = 'none';
} else if (corner.class === 'top-right') {
marker.style.borderLeft = 'none';
marker.style.borderBottom = 'none';
} else if (corner.class === 'bottom-left') {
marker.style.borderRight = 'none';
marker.style.borderTop = 'none';
} else if (corner.class === 'bottom-right') {
marker.style.borderLeft = 'none';
marker.style.borderTop = 'none';
}
scanBox.appendChild(marker);
});
}
/**
* 启动扫描线动画 - 优化版本
*/
startScanLineAnimation() {
if (!this.scanLine) return;
let direction = 1;
let position = 10;
const maxPosition = this.scanLine.parentElement.offsetHeight - 30;
let lastTime = 0;
const animationInterval = 50; // 动画间隔,降低帧率
const animate = (currentTime) => {
if (currentTime - lastTime >= animationInterval) {
position += direction * 3;
if (position >= maxPosition) {
direction = -1;
} else if (position <= 10) {
direction = 1;
}
this.scanLine.style.top = position + 'px';
lastTime = currentTime;
}
this.scanLineAnimation = requestAnimationFrame(animate);
};
this.scanLineAnimation = requestAnimationFrame(animate);
}
/**
* 停止扫描线动画
*/
stopScanLineAnimation() {
if (this.scanLineAnimation) {
cancelAnimationFrame(this.scanLineAnimation);
this.scanLineAnimation = null;
}
}
/**
* 添加事件监听
*/
addEventListeners(backButton, selectImage) {
// 返回按钮点击事件处理
const handleBackClick = (e) => {
e.preventDefault();
e.stopPropagation();
console.log('返回按钮被点击');
// 先派发关闭事件,让主逻辑处理
const event = new CustomEvent('jz-scanner-close');
document.dispatchEvent(event);
};
// 同时监听click和touchend事件,确保移动端兼容性
backButton.addEventListener('click', handleBackClick);
backButton.addEventListener('touchend', handleBackClick);
// 选择图片按钮事件处理
const handleSelectImage = (e) => {
e.preventDefault();
e.stopPropagation();
console.log('相册按钮被点击');
this.selectImageFile();
};
selectImage.addEventListener('click', handleSelectImage);
selectImage.addEventListener('touchend', handleSelectImage);
// 添加悬停效果
backButton.addEventListener('mouseenter', () => {
backButton.style.background = 'rgba(255,255,255,0.2)';
});
backButton.addEventListener('mouseleave', () => {
backButton.style.background = 'rgba(255,255,255,0.1)';
});
selectImage.addEventListener('mouseenter', () => {
selectImage.style.background = 'rgba(255,255,255,0.2)';
});
selectImage.addEventListener('mouseleave', () => {
selectImage.style.background = 'rgba(255,255,255,0.1)';
});
}
/**
* 选择图片文件
*/
selectImageFile() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.style.display = 'none';
input.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
const customEvent = new CustomEvent('jz-scanner-file-selected', {
detail: { file }
});
document.dispatchEvent(customEvent);
}
});
document.body.appendChild(input);
input.click();
document.body.removeChild(input);
}
/**
* 获取video元素用于摄像头显示
*/
getVideoElement() {
return this.video;
}
/**
* 移除扫码器UI
*/
removeScannerUI() {
this.stopScanLineAnimation();
if (this.overlay && this.overlay.parentNode) {
this.addHideAnimation(() => {
this.overlay.parentNode.removeChild(this.overlay);
this.overlay = null;
this.canvas = null;
this.video = null;
this.scanLine = null;
// 恢复页面滚动
document.body.style.overflow = '';
// 移除全局样式
this.removeGlobalStyles();
console.log('扫码器UI已移除');
});
}
}
/**
* 添加显示动画
*/
addShowAnimation() {
if (!this.overlay) return;
this.overlay.style.opacity = '0';
this.overlay.style.transform = 'scale(1.1)';
this.overlay.style.transition = 'all 0.3s ease-out';
// 强制重排
this.overlay.offsetHeight;
this.overlay.style.opacity = '1';
this.overlay.style.transform = 'scale(1)';
}
/**
* 添加隐藏动画
*/
addHideAnimation(callback) {
if (!this.overlay) {
callback && callback();
return;
}
this.overlay.style.transition = 'all 0.3s ease-in';
this.overlay.style.opacity = '0';
this.overlay.style.transform = 'scale(0.9)';
setTimeout(() => {
callback && callback();
}, 300);
}
/**
* 添加全局样式
*/
addGlobalStyles() {
if (document.getElementById('jz-scanner-styles')) return;
const style = document.createElement('style');
style.id = 'jz-scanner-styles';
style.textContent = `
jz-scan-line {
0% { transform: translateY(-100%); }
100% { transform: translateY(calc(100vh - 200px)); }
}
jz-error-shake {
0% { transform: translate(-50%, -50%) translateX(0); }
25% { transform: translate(-50%, -50%) translateX(-10px); }
50% { transform: translate(-50%, -50%) translateX(10px); }
75% { transform: translate(-50%, -50%) translateX(-5px); }
100% { transform: translate(-50%, -50%) translateX(0); }
}
.jz-scanner-fullscreen * {
box-sizing: border-box;
}
(max-width: 480px) {
.jz-scanner-navbar {
height: 50px !important;
padding: 0 15px !important;
}
.jz-scanner-title {
font-size: 16px !important;
}
.jz-scanner-select {
font-size: 14px !important;
padding: 6px 10px !important;
}
.jz-scanner-hint {
font-size: 14px !important;
}
}
`;
document.head.appendChild(style);
}
/**
* 移除全局样式
*/
removeGlobalStyles() {
const style = document.getElementById('jz-scanner-styles');
if (style) {
style.parentNode.removeChild(style);
}
}
/**
* 显示扫码成功效果(已禁用)
*/
showSuccessEffect() {
// 不再显示成功弹窗,扫码成功后直接返回结果
console.log('扫码成功,直接返回结果');
}
/**
* 显示错误信息
*/
showErrorMessage(message) {
if (!this.overlay) return;
const errorTip = document.createElement('div');
errorTip.className = 'jz-scanner-error';
errorTip.innerHTML = `
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#f44336" stroke-width="3">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
<div>${message}</div>
`;
errorTip.style.cssText = `
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(255, 255, 255, 0.95);
color: #f44336;
padding: 20px;
border-radius: 10px;
font-size: 16px;
font-weight: bold;
z-index: 10002;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
animation: jz-error-shake 0.6s ease-out;
max-width: 300px;
text-align: center;
`;
this.overlay.appendChild(errorTip);
setTimeout(() => {
if (errorTip.parentNode) {
errorTip.parentNode.removeChild(errorTip);
}
}, 3000);
}
/**
* 获取当前canvas元素
*/
getCanvas() {
return this.canvas;
}
/**
* 检查UI是否已创建
*/
isUICreated() {
return !!(this.overlay && this.canvas);
}
}
export default UIManager;