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 {
min-width: 300px;
max-width: 400px;
height: 400px;
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;
margin-bottom: 15px;
}
#replaceToggleContainer {
display: flex !important;
align-items: center;
margin: 10px 0;
}
.replace-toggle-group {
display: flex;
align-items: center;
}
#replaceToggleContainer .switch {
display: block;
position: relative;
width: 60px;
height: 34px;
margin: 0 10px;
}
#addCompareBtn {
display: none;
margin-left: 15px;
}
.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;
}
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
display: none;
}
.dialog-content {
background: white;
padding: 20px;
border-radius: 5px;
width: 80%;
max-width: 600px;
max-height: 80vh;
overflow: auto;
}
.dialog-buttons {
display: flex;
justify-content: flex-end;
margin-top: 15px;
gap: 10px;
}
.compare-table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
}
.compare-table th, .compare-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.compare-table th {
background-color: #f2f2f2;
}
.compare-table td {
padding: 0px;
}
.compare-table td:first-child {
width: 100%;
}
.compare-table input {
white-space: pre;
min-height: 34px;
}
.barcode-input {
width: 100%;
min-height: 28px;
padding: 5px;
box-sizing: border-box;
border: 1px solid #ddd;
resize: vertical;
font-family: inherit;
}
.compare-table {
width: 100%;
border-collapse: collapse;
}
.compare-table td {
padding: 8px;
border: 1px solid #ddd;
}
.compare-table td:first-child {
width: 100%;
}
.compare-table input {
width: 100%;
height: 100%;
padding: 5px;
box-sizing: border-box;
border: 1px solid #ddd;
border-radius: 0;
margin-bottom: 0px;
}
.compare-table .delete-btn {
padding: 3px 8px;
font-size: 12px;
background: #ff4444;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
.compare-table .delete-btn:hover {
background: #cc0000;
}
</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 style="height: 400px;"></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>
<option value="compare">内容对比</option>
</select>
</div>
<div id="replaceToggleContainer">
<div class="replace-toggle-group">
<label for="replaceToggle">字符替换:</label>
<label class="switch">
<input type="checkbox" id="replaceToggle" checked>
<span class="slider"></span>
</label>
</div>
<button class="button" id="addCompareBtn">管理对比内容</button>
</div>
<div>
<label>当前结果:</label>
<pre id="result"></pre>
<button class="button" id="copyButton">复制</button>
</div>
<div id="toast" class="toast"></div>
<div id="compareDialog" class="dialog-overlay">
<div class="dialog-content">
<h3>对比内容</h3>
<table id="compareTable" class="compare-table">
<thead>
<tr>
<th>条码内容</th>
<th style="width:80px;">操作</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div class="dialog-buttons">
<button class="button" id="saveCompareBtn">保存</button>
<button class="button button-outline" id="cancelCompareBtn">取消</button>
</div>
</div>
</div>
<audio id="beep" class="beep" src="https://down.ear0.com:3321/index/preview?soundid=20668&type=mp3&audio=sound.mp3&token=czovL2Rvd24uZWFyMC5jb206MzMyMS9pbmRleC9wcmV2aWV3P3NvdW5kaWQ9MjA2NjgmdHlwZT1tcDMmYXVkaW89c291bmQubXAz&sound=audio.mp3nd=audio.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 currentMode = '';
let isScanning = false;
let isFlashOn = false;
let videoTrack = null;
let audioCtx;
let isDialogOpen = false;
window.addEventListener('load', function() {
initScanner();
setupEventListeners();
checkMobileDevice();
initCompareDialog();
const initialMode = document.getElementById('decoding-style').value;
updateUIForMode(initialMode);
});
function initScanner() {
codeReader = new ZXing.BrowserQRCodeReader();
console.log('ZXing 二维码阅读器已初始化');
codeReader.getVideoInputDevices()
.then(videoInputDevices => {
const sourceSelect = document.getElementById('sourceSelect');
if (videoInputDevices.length >= 1) {
selectedDeviceId = videoInputDevices[videoInputDevices.length - 1].deviceId;
videoInputDevices.forEach(device => {
const option = document.createElement('option');
option.value = device.deviceId;
option.text = device.label || `摄像头 ${sourceSelect.length + 1}`;
sourceSelect.appendChild(option);
});
const select_options = sourceSelect.children;
select_options[select_options.length - 1].selected = true;
console.log(videoInputDevices[videoInputDevices.length - 1]);
document.getElementById('sourceSelectPanel').style.display = 'block';
} else {
showToast('未找到摄像头设备');
}
})
.catch(err => {
console.error('获取摄像头失败:', err);
showToast('无法访问摄像头,请检查权限设置');
});
}
function initCompareDialog() {
const dialog = document.getElementById('compareDialog');
const tbody = document.querySelector('#compareTable tbody');
document.getElementById('saveCompareBtn').addEventListener('click', saveCompareContent);
document.getElementById('cancelCompareBtn').addEventListener('click', closeCompareDialog);
dialog.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && e.target.tagName === 'INPUT') {
e.preventDefault();
saveCompareContent();
}
});
}
function setupEventListeners() {
document.getElementById('sourceSelect').addEventListener('change', function() {
selectedDeviceId = this.value;
});
document.getElementById('decoding-style').addEventListener('change', function() {
currentMode = this.value;
updateUIForMode(currentMode);
if (isScanning) {
resetScanner();
}
});
document.getElementById('startButton').addEventListener('click', startScanning);
document.getElementById('resetButton').addEventListener('click', resetScanner);
document.getElementById('copyButton').addEventListener('click', copyResult);
document.getElementById('flashToggle').addEventListener('click', toggleFlash);
document.getElementById('addCompareBtn').addEventListener('click', openCompareDialog);
}
function updateUIForMode(mode) {
const replaceToggleGroup = document.querySelector('.replace-toggle-group');
const addCompareBtn = document.getElementById('addCompareBtn');
replaceToggleGroup.style.display = mode === 'once' ? 'block' : 'none';
addCompareBtn.style.display = mode === 'compare' ? 'inline-block' : 'none';
if (mode !== 'once') {
document.getElementById('replaceToggle').checked = false;
}
}
function openCompareDialog() {
isDialogOpen = true;
const dialog = document.getElementById('compareDialog');
renderCompareTable();
dialog.style.display = 'flex';
}
function closeCompareDialog() {
isDialogOpen = false;
document.getElementById('compareDialog').style.display = 'none';
}
let barcodeList = [];
let isProcessingMultiLine = false;
function renderCompareTable() {
const tbody = document.querySelector('#compareTable tbody');
tbody.innerHTML = '';
if (barcodeList.length === 0) {
barcodeList.push({ id: Date.now(), value: '' });
}
barcodeList.forEach((item) => {
const row = document.createElement('tr');
row.innerHTML = `
<td><textarea class="barcode-input" data-id="${item.id}">${escapeHtml(item.value)}</textarea></td>
<td><button class="delete-btn" data-id="${item.id}">删除</button></td>
`;
tbody.appendChild(row);
const textarea = row.querySelector('textarea');
textarea.addEventListener('keydown', handleInputKeyDown);
textarea.addEventListener('blur', handleInputBlur);
});
const inputs = tbody.querySelectorAll('textarea');
if (inputs.length > 0) {
inputs[inputs.length - 1].focus();
}
}
function handleInputKeyDown(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
processCurrentInput(e.target);
}
}
function handleInputBlur(e) {
if (!isProcessingMultiLine) {
processCurrentInput(e.target, false);
}
}
function processCurrentInput(inputElement, autoAddNew = true) {
if (isProcessingMultiLine) return;
isProcessingMultiLine = true;
const currentId = parseInt(inputElement.getAttribute('data-id'));
const value = inputElement.value.trim();
if (value.includes('\n')) {
const lines = value.split('\n').filter(line => line.trim() !== '');
updateBarcodeItem(currentId, lines[0] || '');
for (let i = 1; i < lines.length; i++) {
addNewBarcodeItem(lines[i]);
}
if (autoAddNew && lines.length > 0) {
addNewBarcodeItem('');
}
}
else {
updateBarcodeItem(currentId, value);
if (autoAddNew && value !== '') {
addNewBarcodeItem('');
}
}
isProcessingMultiLine = false;
renderCompareTable();
}
function updateBarcodeItem(id, value) {
const item = barcodeList.find(item => item.id === id);
if (item) {
item.value = value;
}
}
function addNewBarcodeItem(value) {
barcodeList.push({
id: Date.now() + barcodeList.length,
value: value
});
}
function saveCompareContent() {
document.querySelectorAll('.barcode-input').forEach(input => {
const id = parseInt(input.getAttribute('data-id'));
const value = input.value.trim();
updateBarcodeItem(id, value);
});
barcodeList = barcodeList.filter(item => item.value !== '');
if (barcodeList.length === 0) {
barcodeList.push({ id: Date.now(), value: '' });
}
showToast(`已保存 ${barcodeList.filter(item => item.value !== '').length} 条对比条码`);
closeCompareDialog();
}
function startScanning() {
if (isScanning) return;
currentMode = document.getElementById('decoding-style').value;
if (currentMode === 'compare') {
const hasValidBarcodes = barcodeList.some(item => item.value.trim() !== '');
if (!hasValidBarcodes) {
showToast('请先添加并保存对比条码');
return;
}
}
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 if (currentMode === 'continuously') {
decodeContinuously();
} else if (currentMode === 'compare') {
decodeAndCompare();
}
}
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(async (result) => {
await playBeep(3951.07, 0.05);
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;
document.getElementById('result').textContent = text;
}
if (err && !(err instanceof ZXing.NotFoundException)) {
console.error('连续解码错误:', err);
}
});
}
function decodeAndCompare() {
codeReader.decodeFromInputVideoDeviceContinuously(selectedDeviceId, 'video', async (result, err) => {
if (result) {
const scannedText = result.text.trim();
let found = false;
for (const item of barcodeList) {
if (scannedText === item.value) {
found = true;
break;
}
}
if (found) {
await playBeep(3951.07, 0.05);
document.getElementById('result').textContent = `✓ 匹配: ${scannedText}`;
document.getElementById('result').style.color = '#4CAF50';
} else {
document.getElementById('result').textContent = `✗ 未匹配: ${scannedText}`;
document.getElementById('result').style.color = '#F44336';
}
}
if (err && !(err instanceof ZXing.NotFoundException)) {
console.error('对比识别错误:', err);
}
});
}
function applyReplacements(text) {
return text.split('').map(char => replacements[char] || char).join('');
}
function playBeep(freq = 440, duration = 0.5) {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(freq, audioCtx.currentTime);
gainNode.gain.setValueAtTime(0.8, audioCtx.currentTime);
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.start();
oscillator.stop(audioCtx.currentTime + duration);
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
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;
}
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>