您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Collects barcodes automatically from a table, timestamps them, and provides a viewing interface.
当前为
// ==UserScript== // @name Barcode Collector // @namespace http://tampermonkey.net/ // @version 1.2 // @description Collects barcodes automatically from a table, timestamps them, and provides a viewing interface. // @author Hamad AlShegifi // @match *://his.kaauh.org/lab/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @license MIT // ==/UserScript== (function() { 'use strict'; // --- Configuration --- const TABLE_BODY_SELECTOR = 'tbody[formarrayname="TubeTypeList"]'; const MENU_INJECTION_SELECTOR = 'span.csi-menu-text[title="Documents"]'; const STORAGE_KEY = 'collectedBarcodes_storage'; // --- State Flags --- let isTableObserverAttached = false; let isMenuViewAdded = false; // --- In-memory cache for duplicate checking --- const collectedBarcodesThisSession = new Set(); // --- Main Logic --- // Function to initialize the script and set up observers function initialize() { console.log("Barcode Collector: Script started. Looking for elements..."); // Use a MutationObserver to watch for dynamically loaded elements const observer = new MutationObserver((mutations, obs) => { const tableBody = document.querySelector(TABLE_BODY_SELECTOR); const documentsMenu = document.querySelector(MENU_INJECTION_SELECTOR); // Setup observer for the barcode table if (tableBody && !isTableObserverAttached) { console.log("Barcode Collector: Table body found. Attaching observer."); scanForExistingBarcodes(tableBody); // Scan for any barcodes already on the page setupTableObserver(tableBody); // Observe for new barcodes being added isTableObserverAttached = true; } // Add the custom menu item if (documentsMenu && !isMenuViewAdded) { console.log("Barcode Collector: 'Documents' menu found. Adding custom view menu."); addMenuView(documentsMenu); isMenuViewAdded = true; } // Stop observing if both elements are found and processed if (isTableObserverAttached && isMenuViewAdded) { console.log("Barcode Collector: Initialization complete. Disconnecting observer."); obs.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true }); } // Scan for barcodes that might already be on the page when the script loads function scanForExistingBarcodes(tableBody) { const rows = tableBody.querySelectorAll('tr'); rows.forEach(row => { const barcodeInput = row.querySelector('input[formcontrolname="Barcode"]'); if (barcodeInput && barcodeInput.value) { saveBarcode(barcodeInput.value.trim()); } }); console.log(`Barcode Collector: Scanned and processed ${rows.length} existing rows.`); } // Observe the table for newly added rows function setupTableObserver(tableBody) { const tableObserver = new MutationObserver((mutations) => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { // Ensure we are only looking at element nodes (like TRs) if (node.nodeType === 1) { const barcodeInput = node.querySelector('input[formcontrolname="Barcode"]'); if (barcodeInput && barcodeInput.value) { saveBarcode(barcodeInput.value.trim()); } } }); }); }); tableObserver.observe(tableBody, { childList: true, subtree: false // We only care about direct children (TRs) being added to the TBODY }); } // Function to save a new barcode to storage, with duplicate checking async function saveBarcode(barcode) { // Use the session set to prevent saving duplicates from rapid re-renders if (collectedBarcodesThisSession.has(barcode)) { return; } let barcodes = await GM_getValue(STORAGE_KEY, []); // Also check persistent storage to be absolutely sure if (barcodes.some(entry => entry.barcode === barcode)) { collectedBarcodesThisSession.add(barcode); // Add to session cache to avoid future checks return; } const newEntry = { count: barcodes.length + 1, barcode: barcode, timestamp: new Date().toISOString() }; barcodes.push(newEntry); await GM_setValue(STORAGE_KEY, barcodes); // Save the updated array collectedBarcodesThisSession.add(barcode); // Add to session cache console.log(`Barcode Collector: Saved new barcode - ${barcode}`); } // Function to create and inject the "View Barcodes" menu item function addMenuView(targetMenuElement) { // Find the main container of the "Documents" menu item to ensure proper alignment const documentsContainer = targetMenuElement.closest('csi-main-menu')?.parentElement?.parentElement; if (!documentsContainer) { console.error("Barcode Collector: Could not find the main menu item container to inject into. The page structure may have changed."); return; } // Create a new container that is the same type as the one we found. const newContainer = document.createElement(documentsContainer.tagName); // Set the inner HTML to replicate the structure of the existing menu items, // ensuring it has an icon holder and a text wrapper. newContainer.innerHTML = ` <div class="${documentsContainer.firstElementChild.className}"> <csi-main-menu class="${targetMenuElement.closest('csi-main-menu').className}"> <a style="cursor: pointer;"> <span class="icon-holder csi-menu-icon"> <i class="fa fa-list-ul" aria-hidden="true"></i> </span> <div class="csi-menu-text-wrapper"> <span class="csi-menu-text sidemenu-title" title="Collected Barcodes">Collected Barcodes</span> </div> </a> </csi-main-menu> </div> `; // Add a click listener to the `<a>` tag within our newly created element. newContainer.querySelector('a').addEventListener('click', showBarcodesModal); // Insert the new menu item into the DOM right after the "Documents" menu item. documentsContainer.parentNode.insertBefore(newContainer, documentsContainer.nextSibling); } // --- Helper Function for Time Formatting --- function formatTimeSince(isoTimestamp) { const date = new Date(isoTimestamp); const now = new Date(); const seconds = Math.floor((now - date) / 1000); if (seconds < 60) return `${seconds} seconds ago`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes} minutes ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours} hours ago`; const days = Math.floor(hours / 24); return `${days} days ago`; } // Function to display the modal with collected barcodes async function showBarcodesModal() { const existingModal = document.getElementById('barcode-viewer-modal'); if (existingModal) existingModal.remove(); const barcodes = await GM_getValue(STORAGE_KEY, []); // Sort by count ascending, so the earliest collected are first barcodes.sort((a, b) => a.count - b.count); const modal = document.createElement('div'); modal.id = 'barcode-viewer-modal'; let tableRows = barcodes.map(entry => ` <tr data-barcode-row="${entry.barcode}"> <td>${entry.count}</td> <td>${entry.barcode}</td> <td>${new Date(entry.timestamp).toLocaleString()}</td> <td data-timestamp="${entry.timestamp}">${formatTimeSince(entry.timestamp)}</td> <td class="action-cell-bc"><span class="delete-barcode-btn" data-barcode="${entry.barcode}" title="Delete this barcode">×</span></td> </tr> `).join(''); if (barcodes.length === 0) { tableRows = '<tr><td colspan="5">No barcodes have been collected yet.</td></tr>'; } modal.innerHTML = ` <div class="modal-content-bc"> <div class="modal-header-bc"> <h2>Collected Barcodes</h2> <span class="close-button-bc">×</span> </div> <div class="modal-body-bc"> <table> <thead> <tr> <th>Count</th> <th>Barcode</th> <th>Timestamp</th> <th>Time Since Added</th> <th>Actions</th> </tr> </thead> <tbody>${tableRows}</tbody> </table> </div> <div class="modal-footer-bc"> <button id="clear-barcodes-btn">Clear All Barcodes</button> </div> </div> `; document.body.appendChild(modal); // --- Event Listener for Deleting Individual Barcodes --- const tableBody = modal.querySelector('tbody'); tableBody.addEventListener('click', async function(event) { if (event.target.classList.contains('delete-barcode-btn')) { const barcodeToDelete = event.target.dataset.barcode; await deleteBarcode(barcodeToDelete); // Refresh the modal content without closing it const updatedBarcodes = await GM_getValue(STORAGE_KEY, []); updatedBarcodes.sort((a, b) => a.count - b.count); // Re-sort let newTableRows = updatedBarcodes.map(entry => ` <tr data-barcode-row="${entry.barcode}"> <td>${entry.count}</td> <td>${entry.barcode}</td> <td>${new Date(entry.timestamp).toLocaleString()}</td> <td data-timestamp="${entry.timestamp}">${formatTimeSince(entry.timestamp)}</td> <td class="action-cell-bc"><span class="delete-barcode-btn" data-barcode="${entry.barcode}" title="Delete this barcode">×</span></td> </tr> `).join(''); if (updatedBarcodes.length === 0) { newTableRows = '<tr><td colspan="5">No barcodes have been collected yet.</td></tr>'; } tableBody.innerHTML = newTableRows; } }); // Auto-update the "Time Since Added" column every 5 seconds const timeSinceInterval = setInterval(() => { modal.querySelectorAll('td[data-timestamp]').forEach(cell => { cell.textContent = formatTimeSince(cell.dataset.timestamp); }); }, 5000); modal.querySelector('.close-button-bc').addEventListener('click', () => { clearInterval(timeSinceInterval); // Stop the auto-update modal.remove(); }); const clearButton = modal.querySelector('#clear-barcodes-btn'); clearButton.addEventListener('click', () => { clearInterval(timeSinceInterval); // Stop the auto-update showClearConfirmation(modal); }); } // --- New Function to Delete a Single Barcode --- async function deleteBarcode(barcodeToDelete) { let barcodes = await GM_getValue(STORAGE_KEY, []); // Filter out the barcode to delete let updatedBarcodes = barcodes.filter(entry => entry.barcode !== barcodeToDelete); // Re-calculate the 'count' for the remaining items updatedBarcodes.forEach((entry, index) => { entry.count = index + 1; }); await GM_setValue(STORAGE_KEY, updatedBarcodes); console.log(`Barcode Collector: Deleted barcode - ${barcodeToDelete}`); } // Function to show a confirmation dialog inside the modal function showClearConfirmation(modal) { const modalBody = modal.querySelector('.modal-body-bc'); const modalFooter = modal.querySelector('.modal-footer-bc'); modalBody.innerHTML = ` <p class="confirm-text-bc"> Are you sure you want to delete all collected barcodes?<br>This action cannot be undone. </p>`; modalFooter.innerHTML = ` <button id="confirm-clear-btn" class="modal-btn-bc confirm-btn-bc">Yes, Clear All</button> <button id="cancel-clear-btn" class="modal-btn-bc cancel-btn-bc">Cancel</button>`; modal.querySelector('#confirm-clear-btn').addEventListener('click', async () => { await GM_setValue(STORAGE_KEY, []); modal.remove(); showBarcodesModal(); }); modal.querySelector('#cancel-clear-btn').addEventListener('click', () => { modal.remove(); showBarcodesModal(); }); } // --- Styling for the Modal --- GM_addStyle(` #barcode-viewer-modal { position: fixed; z-index: 10000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; font-family: Arial, sans-serif; } .modal-content-bc { background-color: #fefefe; border-radius: 8px; box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); width: 80%; max-width: 600px; max-height: 80vh; display: flex; flex-direction: column; } .modal-header-bc { padding: 10px 16px; background-color: #5c6bc0; color: white; border-top-left-radius: 8px; border-top-right-radius: 8px; display: flex; justify-content: space-between; align-items: center; } .modal-header-bc h2 { margin: 0; font-size: 1.25em; } .close-button-bc { color: #fff; font-size: 28px; font-weight: bold; cursor: pointer; } .close-button-bc:hover { color: #f1f1f1; } .modal-body-bc { padding: 16px; overflow-y: auto; } .modal-body-bc table { width: 100%; border-collapse: collapse; } .modal-body-bc th, .modal-body-bc td { border: 1px solid #ddd; padding: 8px; text-align: left; } .modal-body-bc th { background-color: #f2f2f2; } .modal-footer-bc { padding: 10px 16px; text-align: right; border-top: 1px solid #ddd; display: flex; justify-content: flex-end; gap: 10px; } #clear-barcodes-btn { background-color: #ef5350; color: white; border: none; padding: 10px 15px; border-radius: 5px; cursor: pointer; font-weight: bold; } #clear-barcodes-btn:hover { background-color: #d32f2f; } .confirm-text-bc { text-align: center; font-size: 1.1em; margin: 20px; } .modal-btn-bc { border: none; padding: 10px 15px; border-radius: 5px; cursor: pointer; font-weight: bold; } .confirm-btn-bc { background-color: #ef5350; color: white; } .confirm-btn-bc:hover { background-color: #d32f2f; } .cancel-btn-bc { background-color: #e0e0e0; color: #333; } .cancel-btn-bc:hover { background-color: #bdbdbd; } .action-cell-bc { text-align: center !important; } .delete-barcode-btn { cursor: pointer; font-weight: bold; font-size: 20px; color: #ef5350; padding: 0 5px; border-radius: 4px; } .delete-barcode-btn:hover { color: white; background-color: #d32f2f; } `); // --- Start the script --- initialize(); })();