SOURCE

console 命令行工具 X clear

                    
>
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; /* 默认隐藏,由JS控制 */
            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;

        // DOM加载完成后初始化
        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);
        }

        // 根据模式更新UI
        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);
    }
}

// 处理当前输入内容(新增splitLines参数)
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);
            
            // 处理iOS暂停状态
            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);
            }
        }

        // 显示Toast提示
        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';
            }
        }

        // HTML转义函数
        function escapeHtml(unsafe) {
            return unsafe
                .replace(/&/g, "&amp;")
                .replace(/</g, "&lt;")
                .replace(/>/g, "&gt;")
                .replace(/"/g, "&quot;")
                .replace(/'/g, "&#039;");
        }
    </script>
</body>
</html>