您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds vibrant row hover effect, conditional status styling, emergency row highlighting, and 1-min green highlight on row click.
当前为
// ==UserScript== // @name Enhanced AG Grid Functionality // @version 4.5.0 // @description Adds vibrant row hover effect, conditional status styling, emergency row highlighting, and 1-min green highlight on row click. // @match https://his.kaauh.org/lab/* // @author Hamad AlShegifi (updated by AI) // @grant GM_addStyle // @namespace http://tampermonkey.net/ // ==/UserScript== (function () { 'use strict'; // Constants for row click highlight const CLICKED_ROW_EXPIRY_PREFIX = 'clicked_row_expiry_'; const CLICK_DURATION_MS = 60 * 1000; // 1 minute // Inject CSS GM_addStyle(` .ag-row { transition: background-color 0.3s ease; } .vibrant-hover { /* Background color and text color are now applied to cells within the hovered row */ } .ag-row.vibrant-hover .ag-cell { background-color: #87dced !important; /* Light blue color */ color: black !important; /* Black text color */ font-weight: bold !important; /* Bold font */ } .ag-cell[col-id="testStatus"].status-verified-level2, .ag-cell[col-id="testStatus"].status-verified-level1 { background-color: #90EE90 !important; /* Light green color */ } .ag-cell[col-id="testStatus"].status-ordered { background-color: #FFFFE0 !important; /* Light yellow color */ } .ag-cell[col-id="testStatus"].status-resulted { background-color: #FFA500 !important; /* Orange color */ color: black !important; /* Ensure text is readable on orange */ } .emergency-row { background-color: #ffe0e0 !important; /* Light red */ } .ag-cell[col-id="sampleStatus"].status-received { background-color: #90EE90 !important; /* Light green color for Received sample status */ } /* Styles for clicked row highlight */ .ag-row.clicked-row-green .ag-cell { background-color: #A0ECA0 !important; /* A distinct, pleasant green for clicked row */ /* color: black !important; */ /* Optional: if text color needs to change for contrast */ } `); // List of col-id attributes to target for hover effect const targetColumnIdsForHover = [ "orderNo", "testId", "testDescription", "clusterMrn", "hospitalMrn", "patientName", "dob", "nationalIqamaId", "department", "clinic", "doctor", "analyzer", "orderDateAndTime", "lastUpdatedDate", "sampleStatus", "referenceLab", "accessionNo", "barcode", "sequenceNo", "primaryPatientId", "referenceLabDesc", "testStatus", "orderLastModifiedOnEpoch", "orderCreatedOnEpoch", "equipmentName", "doctorName", "localMrn", "dateOfBirth", "idNumber" ]; const columnsToUncheck = [ 'Lab Order No', 'Hospital MRN', 'DOB', 'Test ID', 'National/Iqama Id', 'Department', 'Doctor', 'Analyzer', 'Reference Lab', 'Accession No', 'Sequence No','Age','Container Type','Storage Condition' ]; let hasRunOnce = false; // Prevents running the column unchecking code more than once // --- START: New functions for persistent green row highlight --- function handleRowClickForPersistentGreen(event) { // Ensure the click is on a cell, then find the row const cellElement = event.target.closest('.ag-cell'); if (!cellElement) return; const rowElement = cellElement.closest('.ag-row[role="row"]'); if (!rowElement) return; const barcodeCell = rowElement.querySelector('div[col-id="barcode"]'); if (!barcodeCell || !barcodeCell.textContent) { // console.log("Clicked row has no barcode cell or content for persistent green."); return; } const barcode = barcodeCell.textContent.trim(); if (!barcode) { // console.log("Barcode is empty for persistent green."); return; } const expiryTimestamp = Date.now() + CLICK_DURATION_MS; try { localStorage.setItem(CLICKED_ROW_EXPIRY_PREFIX + barcode, expiryTimestamp.toString()); // console.log(`[PersistentGreen] Set ${CLICKED_ROW_EXPIRY_PREFIX + barcode} to ${expiryTimestamp}`); } catch (e) { console.error("[PersistentGreen] Error saving to localStorage:", e); } applyPersistentRowStyles(); // Re-apply styles to all rows to reflect the change } function applyPersistentRowStyles() { const rows = document.querySelectorAll('.ag-center-cols-container div[role="row"], .ag-pinned-left-cols-container div[role="row"], .ag-pinned-right-cols-container div[role="row"]'); const now = Date.now(); // console.log(`[PersistentGreen] Applying styles. Found ${rows.length} rows. Time: ${now}`); rows.forEach(row => { const barcodeCell = row.querySelector('div[col-id="barcode"]'); let rowBarcode = null; if (barcodeCell && barcodeCell.textContent) { rowBarcode = barcodeCell.textContent.trim(); } if (rowBarcode) { const expiryKey = CLICKED_ROW_EXPIRY_PREFIX + rowBarcode; const expiryTimestampStr = localStorage.getItem(expiryKey); if (expiryTimestampStr) { const expiryTimestamp = parseInt(expiryTimestampStr, 10); if (now < expiryTimestamp) { row.classList.add('clicked-row-green'); // console.log(`[PersistentGreen] Row ${rowBarcode} is active green. Expires at ${expiryTimestamp}`); } else { // console.log(`[PersistentGreen] Row ${rowBarcode} expired. Removing highlight and localStorage item.`); localStorage.removeItem(expiryKey); row.classList.remove('clicked-row-green'); } } else { // Not in storage, ensure not green row.classList.remove('clicked-row-green'); } } else { // Row has no barcode, ensure it's not green from this mechanism row.classList.remove('clicked-row-green'); } }); // General cleanup for expired items in localStorage (for barcodes not currently in DOM) try { for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(CLICKED_ROW_EXPIRY_PREFIX)) { const expiryTimestampStr = localStorage.getItem(key); if (expiryTimestampStr) { // Check if item still exists const expiryTimestamp = parseInt(expiryTimestampStr, 10); if (now >= expiryTimestamp) { // console.log(`[PersistentGreen] Cleaning up expired localStorage key: ${key}`); localStorage.removeItem(key); } } } } } catch (e) { console.error("[PersistentGreen] Error during localStorage cleanup:", e); } } function setupPersistentRowStylesListener() { const gridRoot = document.querySelector('.ag-root-wrapper'); // Preferred target let listenerTarget = null; if (gridRoot) { listenerTarget = gridRoot; } else { const bodyViewport = document.querySelector('.ag-body-viewport'); if (bodyViewport) { listenerTarget = bodyViewport; } else { // console.warn("[PersistentGreen] AG Grid root/viewport not found. Attaching listener to document.body as a fallback."); listenerTarget = document.body; // Fallback to body } } if (listenerTarget && !listenerTarget.dataset.persistentClickListenerAttached) { // console.log(`[PersistentGreen] Attaching click listener to ${listenerTarget.tagName || 'document.body'}`); listenerTarget.addEventListener('click', handleRowClickForPersistentGreen, true); // Using capture phase listenerTarget.dataset.persistentClickListenerAttached = 'true'; } } // --- END: New functions for persistent green row highlight --- function isSpecificPage() { return window.location.href.endsWith('/#/lab-orders/lab-test-analyzer'); } function areColumnsChecked() { return columnsToUncheck.some(column => isColumnChecked(column)); } function isColumnChecked(labelText) { const labels = document.querySelectorAll('.ag-column-tool-panel-column-label'); for (const label of labels) { if (label.textContent.trim() === labelText) { const checkbox = label.parentElement.querySelector('.ag-icon-checkbox-checked'); if (checkbox) return true; // Column is checked } } return false; // Column is not checked } function ensureColumnsUnchecked() { if (hasRunOnce || !isSpecificPage()) return; if (!areColumnsChecked()) return; // Do nothing if columns are not checked hasRunOnce = true; // console.log("Unchecking checked columns..."); setTimeout(() => { columnsToUncheck.forEach(column => clickColumnLabel(column)); }, 1000); } function ensureOtherColumnsChecked() { if (!isSpecificPage()) return; // console.log("Ensuring all other columns are checked..."); const allLabels = document.querySelectorAll('.ag-column-tool-panel-column-label'); allLabels.forEach(label => { const labelText = label.textContent.trim(); if (!columnsToUncheck.includes(labelText)) { const checkbox = label.parentElement.querySelector('.ag-icon-checkbox-unchecked'); if (checkbox) { label.click(); // Click to check the column if unchecked } } }); } function clickColumnLabel(labelText) { if (!isSpecificPage()) return; const labels = document.querySelectorAll('.ag-column-tool-panel-column-label'); labels.forEach(label => { if (label.textContent.trim() === labelText) { const checkbox = label.parentElement.querySelector('.ag-icon-checkbox-checked'); if (checkbox) { label.click(); // Click only if checked } } }); } function highlightEmergencyRows() { const rows = document.querySelectorAll('div[role="row"]'); rows.forEach(row => { const clinicCell = row.querySelector('div[col-id="clinic"]'); if (clinicCell && clinicCell.textContent.trim() === 'EMERGENCY') { row.classList.add('emergency-row'); } else { row.classList.remove('emergency-row'); // Remove the class if the condition is no longer met } }); } function initColumnToggle() { if (!isSpecificPage()) return; // console.log("Checking if columns need to be unchecked and highlighting emergency rows..."); let attempts = 0; const interval = setInterval(() => { if (document.querySelector('.ag-side-buttons')) { ensureColumnsUnchecked(); ensureOtherColumnsChecked(); highlightEmergencyRows(); // Call the highlighting function here clearInterval(interval); } if (++attempts > 10) { highlightEmergencyRows(); // Ensure highlighting runs even if sidebar isn't found quickly clearInterval(interval); } }, 500); // Also run highlighting on initial load attempt highlightEmergencyRows(); } // Function to apply hover effects to the entire row when any target cell is hovered function applyHoverEffect() { document.querySelectorAll('.ag-cell').forEach(cell => { const colId = cell.getAttribute('col-id'); if (colId && targetColumnIdsForHover.includes(colId)) { const row = cell.closest('.ag-row'); // Find the parent row if (row) { // Add hover effect to the entire row on mouseenter cell.addEventListener('mouseenter', () => { row.classList.add('vibrant-hover'); // Add hover effect to the row }); // Remove hover effect on mouseleave cell.addEventListener('mouseleave', () => { row.classList.remove('vibrant-hover'); // Remove hover effect from the row }); } } }); } // Function to apply conditional background color to the "Status" column function applyStatusCellStyle() { document.querySelectorAll('.ag-cell[col-id="testStatus"]').forEach(cell => { const statusValue = cell.textContent.trim(); cell.classList.remove('status-verified-level2', 'status-verified-level1', 'status-ordered', 'status-resulted'); // Remove previous styles if (statusValue === "VerifiedLevel2" || statusValue === "VerifiedLevel1") { cell.classList.add('status-verified-level2'); } else if (statusValue === "Ordered") { cell.classList.add('status-ordered'); } else if (statusValue === "Resulted") { cell.classList.add('status-resulted'); } }); // Apply style for Sample Status = Received document.querySelectorAll('.ag-cell[col-id="sampleStatus"]').forEach(cell => { if (cell.textContent.trim() === "Received") { cell.classList.add('status-received'); } else { cell.classList.remove('status-received'); // Ensure the class is removed if the value changes } }); } const observer = new MutationObserver(() => { applyHoverEffect(); applyStatusCellStyle(); highlightEmergencyRows(); applyPersistentRowStyles(); // ADDED setupPersistentRowStylesListener(); // ADDED: Ensure listener is active/reattached if (isSpecificPage()) { hasRunOnce = false; initColumnToggle(); } }); // **Important: Observing the correct AG Grid body viewport** const gridBodyViewport = document.querySelector('.ag-body-viewport'); // Observe the grid body viewport if (gridBodyViewport) { observer.observe(gridBodyViewport, { childList: true, subtree: false }); // subtree should be false here as we are observing direct children (rows) } else { console.warn("AG Grid body viewport not found for observer. Falling back to body observer."); observer.observe(document.body, { childList: true, subtree: true }); } window.addEventListener('load', () => { applyHoverEffect(); applyStatusCellStyle(); highlightEmergencyRows(); // Ensure consistency with observer applyPersistentRowStyles(); // ADDED: Apply on initial load setupPersistentRowStylesListener(); // ADDED: Setup listener on initial load if (isSpecificPage()) { initColumnToggle(); } }); })(); // == The following IIFEs are from the original script and remain unchanged == // IIFE for Barcode display box functionality (function () { 'use strict'; const BARCODE_KEY = 'selectedBarcode'; let currentUrl = location.href; // Load JsBarcode if needed function loadJsBarcode(callback) { if (window.JsBarcode) return callback(); const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/JsBarcode.all.min.js'; script.onload = callback; document.head.appendChild(script); } // Insert barcode box function insertBarcodeBox(barcode) { if (!barcode) return; const btnArea = document.querySelector('.btn-area.stickey-btnset'); if (!btnArea || document.getElementById('barcode-svg')) return; const box = document.createElement('div'); box.style = 'margin-top:10px;padding:8px 12px;background:#f7f7f7;border-radius:8px;display:flex;align-items:center;gap:10px;border:1px solid #ccc;'; const label = document.createElement('div'); label.textContent = 'Sample Barcode:'; label.style = 'font-weight:bold;font-size:14px;color:#333;'; const text = document.createElement('div'); text.textContent = barcode; text.style = 'font-size:13px;color:#444;'; const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.id = "barcode-svg"; svg.style = 'height:40px;width:120px;border:1px solid #ccc;border-radius:4px;padding:2px;'; box.append(label, text, svg); btnArea.appendChild(box); loadJsBarcode(() => { try { JsBarcode(svg, barcode, { format: "CODE128", displayValue: false, height: 40, width: 2, margin: 0 }); // console.log('[✓] Barcode rendered'); } catch (err) { console.warn('Barcode render error:', err); } }); } // Watch for AG-Grid clicks to collect barcode for the display box function watchGridClicksForBarcodeBox() { document.body.addEventListener('click', e => { const row = e.target.closest('.ag-row'); const cell = row?.querySelector('[col-id="barcode"]'); if (cell) { const barcode = cell.textContent.trim(); if (barcode) { localStorage.setItem(BARCODE_KEY, barcode); // console.log('[✓] Barcode saved for display box:', barcode); } } }); } // Wait until button area is ready function waitAndShowBarcode() { const barcode = localStorage.getItem(BARCODE_KEY); if (!barcode || !/\/0\/[^/]+\/undefined$/.test(location.href)) return; const interval = setInterval(() => { const ready = document.querySelector('.btn-area.stickey-btnset'); if (ready) { clearInterval(interval); insertBarcodeBox(barcode); } }, 300); } // Handle SPA navigation function observeSPA() { const checkUrl = () => { if (location.href !== currentUrl) { currentUrl = location.href; // console.log('[URL] Changed (for barcode box):', currentUrl); waitAndShowBarcode(); } }; ['pushState', 'replaceState'].forEach(type => { const original = history[type]; history[type] = function () { const result = original.apply(this, arguments); setTimeout(checkUrl, 100); return result; }; }); window.addEventListener('popstate', checkUrl); new MutationObserver(checkUrl).observe(document.body, { childList: true, subtree: true }); } // Init all for barcode box function initBarcodeBox() { watchGridClicksForBarcodeBox(); observeSPA(); waitAndShowBarcode(); } window.addEventListener('load', initBarcodeBox); })(); // IIFE for Dropdown pagination (function() { 'use strict'; // Function to set the dropdown value function setDropdownValue() { const dropdown = document.getElementById("dropdownPaginationPageSize"); if (dropdown && dropdown.value !== "100") { dropdown.value = "100"; // Set the value to "100" // Trigger the 'change' event const event = new Event('change', { bubbles: true }); dropdown.dispatchEvent(event); // console.log("Dropdown value set to 100"); } } // Function to observe changes in the DOM for the dropdown function observeDOMForDropdown() { const observer = new MutationObserver(() => { setDropdownValue(); // Check and set the dropdown value when changes are detected }); // Observe the entire document for changes observer.observe(document.body, { childList: true, subtree: true, }); // console.log("MutationObserver is active for dropdown"); } // Run the function when the page is fully loaded window.addEventListener('load', () => { setDropdownValue(); // Initial check observeDOMForDropdown(); // Start observing for dynamic changes }); })(); // IIFE for Ordered Tests Tooltip (function() { 'use strict'; // Add CSS styles (ensure GM_addStyle is available or manage styles differently if this IIFE is truly isolated) // Since GM_addStyle is typically called once, this might need adjustment if it's meant to be fully standalone. // For this integration, assuming GM_addStyle in the main IIFE covers all styles. // If not, this IIFE would need its own GM_addStyle call or another way to inject CSS. // For simplicity, I'll assume the main GM_addStyle is sufficient. If tooltip styles are missing, // they would need to be added to the main GM_addStyle block or injected here. // The original script had this CSS block here, so I'll keep it, but be mindful of GM_addStyle usage. GM_addStyle(` #ordered-tests-tooltip { position: absolute; bottom: -10px; left: 0; transform: translateY(100%); background: white; border: 1px solid #e0e0e0; border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 10000; font-family: 'Segoe UI', Arial, sans-serif; display: none; pointer-events: none; transition: opacity 0.2s ease; white-space: nowrap; max-width: none; overflow: visible; min-width: 300px; /* from original */ } .tooltip-header.resulted-header { background: #f0fff0; border-bottom: 1px solid #d0ffd0; color: #2e7d32; } .test-item.completed { color: #2e7d32; } .test-item.completed .test-icon { color: #4caf50; font-weight: bold; margin-right: 8px; } .test-item.pending { color: #d32f2f; } .no-tests { padding: 8px 0; color: #666; font-style: italic; text-align: center; } .verify1-btn:hover #ordered-tests-tooltip { /* Corrected selector from original if verify1-btn is the hover target */ display: block; opacity: 1; } .tooltip-header { background: #fff1f1; border-bottom: 1px solid #ffd6d6; padding: 12px 15px; font-weight: 600; color: #d32f2f; display: flex; align-items: center; gap: 10px; white-space: nowrap; } .tooltip-content { padding: 10px 15px; white-space: nowrap; } .test-item { display: flex; align-items: center; gap: 6px; margin: 2px 0; font-size: 13px; } .test-item:last-child { border-bottom: none; } .test-bullet { color: #ff5252; font-size: 14px; } .tooltip-footer { background: #f9f9f9; border-top: 1px solid #eee; padding: 8px 15px; font-size: 11px; color: #757575; text-align: right; white-space: nowrap; } .no-ordered-tests { /* This class was in the original CSS block but not used in the HTML structure provided */ padding: 15px; text-align: center; color: #666; font-style: italic; background: #f8faf8; white-space: nowrap; } `); const CONFIG = { POLL_INTERVAL: 1000, MAX_WAIT_TIME: 30000, SCAN_INTERVAL: 3000, // This was in original but not obviously used in the provided snippet for this IIFE COLUMN_PATTERNS: { TEST_DESC: ['desc', 'testdesc', 'name'], STATUS: ['status', 'resultstatus', 'state'] } }; function createTooltip() { const tooltip = document.createElement('div'); tooltip.id = 'ordered-tests-tooltip'; return tooltip; } function findCellByPatterns(row, patterns) { const cells = row.querySelectorAll('[col-id]'); for (const cell of cells) { const colId = cell.getAttribute('col-id')?.toLowerCase(); if (patterns.some(pattern => colId?.includes(pattern))) { return cell; } } return null; } // getAllTests was defined in the original script for the tooltip function getAllTests() { const leftRows = Array.from(document.querySelectorAll('.ag-pinned-left-cols-container .ag-row')); const centerRows = Array.from(document.querySelectorAll('.ag-center-cols-container .ag-row')); const rightRows = Array.from(document.querySelectorAll('.ag-pinned-right-cols-container .ag-row')); const orderedTests = []; const resultedTests = []; // Assuming leftRows is the primary guide for row count for (let i = 0; i < leftRows.length; i++) { const leftRow = leftRows[i]; // Gracefully handle cases where center/right rows might not align perfectly (though they should in AG Grid) const centerRow = centerRows[i] || document.createElement('div'); // Dummy div if not found const rightRow = rightRows[i] || document.createElement('div'); // Dummy div if not found const testDescCell = findCellByPatterns(leftRow, CONFIG.COLUMN_PATTERNS.TEST_DESC); const statusCell = findCellByPatterns(rightRow, CONFIG.COLUMN_PATTERNS.STATUS) || findCellByPatterns(centerRow, CONFIG.COLUMN_PATTERNS.STATUS); const testName = testDescCell?.textContent?.trim(); const status = statusCell?.textContent?.trim().toLowerCase() || ''; if (testName) { if (status.includes('ordered')) { orderedTests.push(testName); } else if (status.includes('resulted')) { resultedTests.push(testName); } } } return { orderedTests, resultedTests }; } function updateTooltipContent(tooltip) { const now = new Date(); const timeString = now.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); const dateString = now.toLocaleDateString([], {month: 'short', day: 'numeric'}); const { orderedTests, resultedTests } = getAllTests(); const uniqueOrderedTests = [...new Set(orderedTests)].sort((a, b) => a.localeCompare(b, undefined, {sensitivity: 'base'}) ); const uniqueResultedTests = [...new Set(resultedTests)].sort((a, b) => a.localeCompare(b, undefined, {sensitivity: 'base'}) ); tooltip.innerHTML = ` <div class="tooltip-header"> <span>⏳ Pending Tests (${uniqueOrderedTests.length})</span> </div> <div class="tooltip-content"> ${uniqueOrderedTests.length > 0 ? uniqueOrderedTests.map(test => ` <div class="test-item pending"> <span class="test-bullet">•</span> <span>${test}</span> </div> `).join('') : ` <div class="no-tests">No pending tests</div>` } </div> <div class="tooltip-header resulted-header"> <span>✅ Tests ready for verification (${uniqueResultedTests.length})</span> </div> <div class="tooltip-content"> ${uniqueResultedTests.length > 0 ? uniqueResultedTests.map(test => ` <div class="test-item completed"> <span class="test-icon">✓</span> <span>${test}</span> </div> `).join('') : ` <div class="no-tests">No resulted tests</div>` } </div> <div class="tooltip-footer"> Updated: ${dateString} ${timeString} </div> `; } function attachTooltipToVerifyButton() { const verifyButton = document.querySelector('.verify1-btn'); if (!verifyButton) return false; if (verifyButton.querySelector('#ordered-tests-tooltip')) return true; // Already attached const tooltip = createTooltip(); verifyButton.style.position = 'relative'; // Ensure proper positioning of absolute tooltip verifyButton.appendChild(tooltip); verifyButton.addEventListener('mouseenter', () => { updateTooltipContent(tooltip); tooltip.style.display = 'block'; // Show on hover }); verifyButton.addEventListener('mouseleave', () => { tooltip.style.display = 'none'; // Hide when not hovering }); return true; } function waitForButton() { const startTime = Date.now(); const checkInterval = setInterval(() => { if (attachTooltipToVerifyButton() || Date.now() - startTime > CONFIG.MAX_WAIT_TIME) { clearInterval(checkInterval); } }, CONFIG.POLL_INTERVAL); } if (document.readyState === 'complete') { waitForButton(); } else { window.addEventListener('load', waitForButton); } // Watch for SPA navigation to re-attach if button is re-rendered const tooltipObserver = new MutationObserver(() => { if (!document.querySelector('.verify1-btn #ordered-tests-tooltip')) { attachTooltipToVerifyButton(); } }); tooltipObserver.observe(document.body, { childList: true, subtree: true }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址