console
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>二维码识别系统</title>
<link rel="stylesheet" href="https://unpkg.com/milligram@1.3.0/dist/milligram.min.css">
<style>
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
#video {
width: 100%;
max-width: 400px;
height: auto;
border: 1px solid #ddd;
background: #000;
}
#result {
min-height: 60px;
padding: 10px;
border: 1px solid #ddd;
background: #f5f5f5;
white-space: pre-wrap;
word-break: break-all;
}
#historyContainer {
margin-top: 15px;
border: 1px solid #ddd;
padding: 10px;
max-height: 200px;
overflow-y: auto;
background: #f9f9f9;
display: none;
}
.history-item {
padding: 8px;
border-bottom: 1px solid #eee;
font-family: monospace;
position: relative;
padding-right: 60px;
}
.history-item:last-child {
border-bottom: none;
}
.history-copy-btn {
position: absolute;
right: 5px;
top: 5px;
padding: 2px 2px;
font-size: 8px;
background: #1E90FF;
border: 1px solid #ddd;
border-radius: 3px;
cursor: pointer;
}
.history-copy-btn:hover {
background: #e0e0e0;
}
.switch-container {
display: flex;
align-items: center;
margin: 10px 0;
}
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
margin: 0 10px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #2196F3;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.7);
color: white;
padding: 10px 20px;
border-radius: 5px;
z-index: 9999;
display: none;
}
.button {
margin-right: 10px;
}
.beep {
display: none;
}
#flashToggle.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.button-row {
margin-bottom: 15px;
}
</style>
</head>
<body>
<main class="container">
<h1>二维码识别</h1>
<p>将二维码转换为产量信息</p>
<div class="button-row">
<button class="button" id="startButton">开始</button>
<button class="button" id="resetButton">重置</button>
<button class="button" id="flashToggle" disabled>开启闪光灯</button>
</div>
<div>
<video id="video" playsinline></video>
</div>
<div id="sourceSelectPanel" style="display:none">
<label for="sourceSelect">选择摄像头:</label>
<select id="sourceSelect"></select>
</div>
<div>
<label for="decoding-style">识别方式:</label>
<select id="decoding-style">
<option value="once">单次识别</option>
<option value="continuously">连续识别</option>
</select>
</div>
<div id="replaceToggleContainer" class="switch-container">
<label for="replaceToggle">字符替换:</label>
<label class="switch">
<input type="checkbox" id="replaceToggle" checked>
<span class="slider"></span>
</label>
</div>
<div>
<label>当前结果:</label>
<pre id="result"></pre>
<button class="button" id="copyButton">复制</button>
</div>
<div id="historyContainer">
<h4>历史记录:</h4>
<button class="button" id="copyAllButton">全部复制</button>
<div id="historyList"></div>
</div>
<div id="toast" class="toast"></div>
<audio id="beep" class="beep" src="https://ppt-mp3cdn.hrxz.com/d/file/filemp3/hrxz.com-3tbklhm0eoh57428.mp3" preload="auto"></audio>
</main>
<script src="https://unpkg.com/@zxing/library@latest"></script>
<script>
const replacements = {
'F': '【结存】:',
'G': '【时段收入】:',
'H': '【累计收入】:',
'I': '【时段产出】:',
'J': '【累计产出】:',
'K': '【高温总数】:',
'L': '【高温超时】:',
'M': '【总计库位】:',
'N': '【空闲库位】:',
'O': '【空托库位】:',
'P': '【实托库位】:',
'Q': '【异常库位】:',
'R': '【入库占用】:',
'S': '【出库占用】:'
};
let codeReader;
let selectedDeviceId;
let scanHistory = new Set();
let currentMode = '';
let isScanning = false;
let isFlashOn = false;
let videoTrack = null;
window.addEventListener('load', function() {
initScanner();
setupEventListeners();
checkMobileDevice();
document.getElementById('replaceToggleContainer').style.display = 'flex';
});
function initScanner() {
codeReader = new ZXing.BrowserQRCodeReader();
console.log('ZXing 二维码阅读器已初始化');
codeReader.getVideoInputDevices()
.then(videoInputDevices => {
const sourceSelect = document.getElementById('sourceSelect');
if (videoInputDevices.length >= 1) {
selectedDeviceId = videoInputDevices[0].deviceId;
videoInputDevices.forEach(device => {
const option = document.createElement('option');
option.value = device.deviceId;
option.text = device.label || `摄像头 ${sourceSelect.length + 1}`;
sourceSelect.appendChild(option);
});
document.getElementById('sourceSelectPanel').style.display = 'block';
} else {
showToast('未找到摄像头设备');
}
})
.catch(err => {
console.error('获取摄像头失败:', err);
showToast('无法访问摄像头,请检查权限设置');
});
}
function setupEventListeners() {
document.getElementById('sourceSelect').addEventListener('change', function() {
selectedDeviceId = this.value;
});
document.getElementById('decoding-style').addEventListener('change', function() {
currentMode = this.value;
const replaceContainer = document.getElementById('replaceToggleContainer');
replaceContainer.style.display = this.value === 'once' ? 'flex' : 'none';
if (this.value !== 'once') {
document.getElementById('replaceToggle').checked = false;
}
if (isScanning) {
resetScanner();
}
});
document.getElementById('startButton').addEventListener('click', startScanning);
document.getElementById('resetButton').addEventListener('click', resetScanner);
document.getElementById('copyButton').addEventListener('click', copyResult);
document.getElementById('copyAllButton').addEventListener('click', copyAllHistory);
document.getElementById('flashToggle').addEventListener('click', toggleFlash);
}
function startScanning() {
if (isScanning) return;
currentMode = document.getElementById('decoding-style').value;
if (currentMode === 'continuously') {
scanHistory.clear();
document.getElementById('historyList').innerHTML = '';
document.getElementById('historyContainer').style.display = 'block';
} else {
document.getElementById('historyContainer').style.display = 'none';
}
if (!selectedDeviceId) {
showToast('请先选择摄像头');
return;
}
isScanning = true;
document.getElementById('startButton').textContent = '扫描中...';
const videoElement = document.getElementById('video');
videoElement.addEventListener('playing', onVideoPlaying, {once: true});
if (currentMode === 'once') {
decodeOnce();
} else {
decodeContinuously();
}
}
function onVideoPlaying() {
const videoElement = document.getElementById('video');
if (videoElement.srcObject) {
videoTrack = videoElement.srcObject.getVideoTracks()[0];
document.getElementById('flashToggle').disabled = false;
checkFlashSupport();
}
}
function checkFlashSupport() {
if (!videoTrack) return;
try {
const capabilities = videoTrack.getCapabilities();
if (!capabilities.torch) {
document.getElementById('flashToggle').disabled = true;
console.log('当前摄像头不支持闪光灯');
}
} catch (err) {
console.error('检查闪光灯支持失败:', err);
document.getElementById('flashToggle').disabled = true;
}
}
function toggleFlash() {
if (!videoTrack) {
showToast('无法访问摄像头,请先开始扫描');
return;
}
if (!('applyConstraints' in videoTrack) || !videoTrack.getCapabilities().torch) {
showToast('当前摄像头不支持闪光灯');
return;
}
isFlashOn = !isFlashOn;
const flashButton = document.getElementById('flashToggle');
try {
videoTrack.applyConstraints({
advanced: [{torch: isFlashOn}]
}).then(() => {
flashButton.textContent = isFlashOn ? '关闭闪光灯' : '开启闪光灯';
showToast(isFlashOn ? '闪光灯已开启' : '闪光灯已关闭');
}).catch(err => {
console.error('闪光灯控制失败:', err);
showToast('闪光灯控制失败');
isFlashOn = false;
flashButton.textContent = '开启闪光灯';
});
} catch (err) {
console.error('闪光灯控制异常:', err);
showToast('闪光灯控制异常');
isFlashOn = false;
flashButton.textContent = '开启闪光灯';
}
}
function decodeOnce() {
codeReader.decodeFromInputVideoDevice(selectedDeviceId, 'video')
.then(result => {
playBeep();
let text = result.text;
if (document.getElementById('replaceToggle').checked) {
text = applyReplacements(text);
}
document.getElementById('result').textContent = text;
isScanning = false;
document.getElementById('startButton').textContent = '开始';
})
.catch(err => {
console.error('解码失败:', err);
document.getElementById('result').textContent = '';
isScanning = false;
document.getElementById('startButton').textContent = '开始';
if (!(err instanceof ZXing.NotFoundException)) {
showToast('解码失败: ' + err.message);
}
});
}
function decodeContinuously() {
codeReader.decodeFromInputVideoDeviceContinuously(selectedDeviceId, 'video', (result, err) => {
if (result) {
const text = result.text;
if (!scanHistory.has(text)) {
playBeep();
scanHistory.add(text);
updateHistoryDisplay(text);
document.getElementById('result').textContent = text;
}
}
if (err && !(err instanceof ZXing.NotFoundException)) {
console.error('连续解码错误:', err);
}
});
}
function applyReplacements(text) {
return text.split('').map(char => replacements[char] || char).join('');
}
function updateHistoryDisplay(text) {
const historyList = document.getElementById('historyList');
const newItem = document.createElement('div');
newItem.className = 'history-item';
newItem.innerHTML = `
${escapeHtml(text)}
<button class="history-copy-btn">复制</button>
`;
historyList.prepend(newItem);
newItem.querySelector('.history-copy-btn').addEventListener('click', function() {
copyToClipboard(text);
showToast('已复制历史记录');
});
}
function copyAllHistory() {
if (scanHistory.size === 0) {
showToast('没有可复制的历史记录');
return;
}
const allText = Array.from(scanHistory).join('\n');
copyToClipboard(allText);
showToast('复制所有历史记录成功');
}
function playBeep() {
const beep = document.getElementById('beep');
beep.currentTime = 0;
beep.play().catch(e => console.log('无法播放提示音:', e));
}
function resetScanner() {
if (codeReader) {
codeReader.reset();
}
if (isFlashOn && videoTrack) {
videoTrack.applyConstraints({
advanced: [{torch: false}]
}).catch(err => console.error('关闭闪光灯失败:', err));
isFlashOn = false;
document.getElementById('flashToggle').textContent = '开启闪光灯';
}
document.getElementById('video').srcObject = null;
videoTrack = null;
document.getElementById('result').textContent = '';
document.getElementById('startButton').textContent = '开始';
document.getElementById('flashToggle').disabled = true;
isScanning = false;
if (currentMode === 'continuously') {
scanHistory.clear();
document.getElementById('historyList').innerHTML = '';
document.getElementById('historyContainer').style.display = 'none';
}
}
function copyResult() {
const resultText = document.getElementById('result').textContent;
if (!resultText) {
showToast('没有可复制的内容');
return;
}
copyToClipboard(resultText);
showToast('复制成功');
}
function copyToClipboard(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
document.body.appendChild(textarea);
if (navigator.userAgent.match(/ipad|iphone/i)) {
const range = document.createRange();
range.selectNodeContents(textarea);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
textarea.setSelectionRange(0, 999999);
} else {
textarea.select();
}
try {
document.execCommand('copy');
} catch (err) {
console.error('复制失败:', err);
throw err;
} finally {
document.body.removeChild(textarea);
}
}
function showToast(message) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.style.display = 'block';
setTimeout(() => {
toast.style.display = 'none';
}, 2000);
}
function checkMobileDevice() {
if (/Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent)) {
document.getElementById('video').style.maxWidth = '300px';
}
}
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
</script>
</body>
</html>